summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorAiden Cline <[email protected]>2026-01-16 13:21:13 -0800
committerGitHub <[email protected]>2026-01-16 15:21:13 -0600
commit8e0ddd1ac99b28784ca2f58bb8779d6353e772ef (patch)
tree890611d67aa58faf8cec874bf49f8958bfd474f1 /packages
parentda78b758d43c3da4a670d6b6421572cf17d37fef (diff)
downloadopencode-8e0ddd1ac99b28784ca2f58bb8779d6353e772ef.tar.gz
opencode-8e0ddd1ac99b28784ca2f58bb8779d6353e772ef.zip
chore: cleanup server routes (#8965)
Co-authored-by: Leka74 <[email protected]> Co-authored-by: Leka74 <[email protected]>
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/server/routes/config.ts92
-rw-r--r--packages/opencode/src/server/routes/experimental.ts156
-rw-r--r--packages/opencode/src/server/routes/file.ts196
-rw-r--r--packages/opencode/src/server/routes/global.ts134
-rw-r--r--packages/opencode/src/server/routes/mcp.ts224
-rw-r--r--packages/opencode/src/server/routes/permission.ts67
-rw-r--r--packages/opencode/src/server/routes/project.ts (renamed from packages/opencode/src/server/project.ts)10
-rw-r--r--packages/opencode/src/server/routes/provider.ts164
-rw-r--r--packages/opencode/src/server/routes/pty.ts168
-rw-r--r--packages/opencode/src/server/routes/question.ts (renamed from packages/opencode/src/server/question.ts)8
-rw-r--r--packages/opencode/src/server/routes/session.ts935
-rw-r--r--packages/opencode/src/server/routes/tui.ts375
-rw-r--r--packages/opencode/src/server/server.ts2400
-rw-r--r--packages/opencode/src/server/tui.ts71
14 files changed, 2557 insertions, 2443 deletions
diff --git a/packages/opencode/src/server/routes/config.ts b/packages/opencode/src/server/routes/config.ts
new file mode 100644
index 000000000..85d28f6aa
--- /dev/null
+++ b/packages/opencode/src/server/routes/config.ts
@@ -0,0 +1,92 @@
+import { Hono } from "hono"
+import { describeRoute, validator, resolver } from "hono-openapi"
+import z from "zod"
+import { Config } from "../../config/config"
+import { Provider } from "../../provider/provider"
+import { mapValues } from "remeda"
+import { errors } from "../error"
+import { Log } from "../../util/log"
+import { lazy } from "../../util/lazy"
+
+const log = Log.create({ service: "server" })
+
+export const ConfigRoutes = lazy(() =>
+ new Hono()
+ .get(
+ "/",
+ describeRoute({
+ summary: "Get configuration",
+ description: "Retrieve the current OpenCode configuration settings and preferences.",
+ operationId: "config.get",
+ responses: {
+ 200: {
+ description: "Get config info",
+ content: {
+ "application/json": {
+ schema: resolver(Config.Info),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ return c.json(await Config.get())
+ },
+ )
+ .patch(
+ "/",
+ describeRoute({
+ summary: "Update configuration",
+ description: "Update OpenCode configuration settings and preferences.",
+ operationId: "config.update",
+ responses: {
+ 200: {
+ description: "Successfully updated config",
+ content: {
+ "application/json": {
+ schema: resolver(Config.Info),
+ },
+ },
+ },
+ ...errors(400),
+ },
+ }),
+ validator("json", Config.Info),
+ async (c) => {
+ const config = c.req.valid("json")
+ await Config.update(config)
+ return c.json(config)
+ },
+ )
+ .get(
+ "/providers",
+ describeRoute({
+ summary: "List config providers",
+ description: "Get a list of all configured AI providers and their default models.",
+ operationId: "config.providers",
+ responses: {
+ 200: {
+ description: "List of providers",
+ content: {
+ "application/json": {
+ schema: resolver(
+ z.object({
+ providers: Provider.Info.array(),
+ default: z.record(z.string(), z.string()),
+ }),
+ ),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ using _ = log.time("providers")
+ const providers = await Provider.list().then((x) => mapValues(x, (item) => item))
+ return c.json({
+ providers: Object.values(providers),
+ default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
+ })
+ },
+ ),
+)
diff --git a/packages/opencode/src/server/routes/experimental.ts b/packages/opencode/src/server/routes/experimental.ts
new file mode 100644
index 000000000..bf66e05cd
--- /dev/null
+++ b/packages/opencode/src/server/routes/experimental.ts
@@ -0,0 +1,156 @@
+import { Hono } from "hono"
+import { describeRoute, validator, resolver } from "hono-openapi"
+import z from "zod"
+import { ToolRegistry } from "../../tool/registry"
+import { Worktree } from "../../worktree"
+import { Instance } from "../../project/instance"
+import { Project } from "../../project/project"
+import { MCP } from "../../mcp"
+import { zodToJsonSchema } from "zod-to-json-schema"
+import { errors } from "../error"
+import { lazy } from "../../util/lazy"
+
+export const ExperimentalRoutes = lazy(() => new Hono()
+ .get(
+ "/tool/ids",
+ describeRoute({
+ summary: "List tool IDs",
+ description:
+ "Get a list of all available tool IDs, including both built-in tools and dynamically registered tools.",
+ operationId: "tool.ids",
+ responses: {
+ 200: {
+ description: "Tool IDs",
+ content: {
+ "application/json": {
+ schema: resolver(z.array(z.string()).meta({ ref: "ToolIDs" })),
+ },
+ },
+ },
+ ...errors(400),
+ },
+ }),
+ async (c) => {
+ return c.json(await ToolRegistry.ids())
+ },
+ )
+ .get(
+ "/tool",
+ describeRoute({
+ summary: "List tools",
+ description:
+ "Get a list of available tools with their JSON schema parameters for a specific provider and model combination.",
+ operationId: "tool.list",
+ responses: {
+ 200: {
+ description: "Tools",
+ content: {
+ "application/json": {
+ schema: resolver(
+ z
+ .array(
+ z
+ .object({
+ id: z.string(),
+ description: z.string(),
+ parameters: z.any(),
+ })
+ .meta({ ref: "ToolListItem" }),
+ )
+ .meta({ ref: "ToolList" }),
+ ),
+ },
+ },
+ },
+ ...errors(400),
+ },
+ }),
+ validator(
+ "query",
+ z.object({
+ provider: z.string(),
+ model: z.string(),
+ }),
+ ),
+ async (c) => {
+ const { provider } = c.req.valid("query")
+ const tools = await ToolRegistry.tools(provider)
+ return c.json(
+ tools.map((t) => ({
+ id: t.id,
+ description: t.description,
+ // Handle both Zod schemas and plain JSON schemas
+ parameters: (t.parameters as any)?._def ? zodToJsonSchema(t.parameters as any) : t.parameters,
+ })),
+ )
+ },
+ )
+ .post(
+ "/worktree",
+ describeRoute({
+ summary: "Create worktree",
+ description: "Create a new git worktree for the current project.",
+ operationId: "worktree.create",
+ responses: {
+ 200: {
+ description: "Worktree created",
+ content: {
+ "application/json": {
+ schema: resolver(Worktree.Info),
+ },
+ },
+ },
+ ...errors(400),
+ },
+ }),
+ validator("json", Worktree.create.schema),
+ async (c) => {
+ const body = c.req.valid("json")
+ const worktree = await Worktree.create(body)
+ return c.json(worktree)
+ },
+ )
+ .get(
+ "/worktree",
+ describeRoute({
+ summary: "List worktrees",
+ description: "List all sandbox worktrees for the current project.",
+ operationId: "worktree.list",
+ responses: {
+ 200: {
+ description: "List of worktree directories",
+ content: {
+ "application/json": {
+ schema: resolver(z.array(z.string())),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ const sandboxes = await Project.sandboxes(Instance.project.id)
+ return c.json(sandboxes)
+ },
+ )
+ .get(
+ "/resource",
+ describeRoute({
+ summary: "Get MCP resources",
+ description: "Get all available MCP resources from connected servers. Optionally filter by name.",
+ operationId: "experimental.resource.list",
+ responses: {
+ 200: {
+ description: "MCP resources",
+ content: {
+ "application/json": {
+ schema: resolver(z.record(z.string(), MCP.Resource)),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ return c.json(await MCP.resources())
+ },
+ )
+)
diff --git a/packages/opencode/src/server/routes/file.ts b/packages/opencode/src/server/routes/file.ts
new file mode 100644
index 000000000..6f4c461e4
--- /dev/null
+++ b/packages/opencode/src/server/routes/file.ts
@@ -0,0 +1,196 @@
+import { Hono } from "hono"
+import { describeRoute, validator, resolver } from "hono-openapi"
+import z from "zod"
+import { File } from "../../file"
+import { Ripgrep } from "../../file/ripgrep"
+import { LSP } from "../../lsp"
+import { Instance } from "../../project/instance"
+import { lazy } from "../../util/lazy"
+
+export const FileRoutes = lazy(() => new Hono()
+ .get(
+ "/find",
+ describeRoute({
+ summary: "Find text",
+ description: "Search for text patterns across files in the project using ripgrep.",
+ operationId: "find.text",
+ responses: {
+ 200: {
+ description: "Matches",
+ content: {
+ "application/json": {
+ schema: resolver(Ripgrep.Match.shape.data.array()),
+ },
+ },
+ },
+ },
+ }),
+ validator(
+ "query",
+ z.object({
+ pattern: z.string(),
+ }),
+ ),
+ async (c) => {
+ const pattern = c.req.valid("query").pattern
+ const result = await Ripgrep.search({
+ cwd: Instance.directory,
+ pattern,
+ limit: 10,
+ })
+ return c.json(result)
+ },
+ )
+ .get(
+ "/find/file",
+ describeRoute({
+ summary: "Find files",
+ description: "Search for files or directories by name or pattern in the project directory.",
+ operationId: "find.files",
+ responses: {
+ 200: {
+ description: "File paths",
+ content: {
+ "application/json": {
+ schema: resolver(z.string().array()),
+ },
+ },
+ },
+ },
+ }),
+ validator(
+ "query",
+ z.object({
+ query: z.string(),
+ dirs: z.enum(["true", "false"]).optional(),
+ type: z.enum(["file", "directory"]).optional(),
+ limit: z.coerce.number().int().min(1).max(200).optional(),
+ }),
+ ),
+ async (c) => {
+ const query = c.req.valid("query").query
+ const dirs = c.req.valid("query").dirs
+ const type = c.req.valid("query").type
+ const limit = c.req.valid("query").limit
+ const results = await File.search({
+ query,
+ limit: limit ?? 10,
+ dirs: dirs !== "false",
+ type,
+ })
+ return c.json(results)
+ },
+ )
+ .get(
+ "/find/symbol",
+ describeRoute({
+ summary: "Find symbols",
+ description: "Search for workspace symbols like functions, classes, and variables using LSP.",
+ operationId: "find.symbols",
+ responses: {
+ 200: {
+ description: "Symbols",
+ content: {
+ "application/json": {
+ schema: resolver(LSP.Symbol.array()),
+ },
+ },
+ },
+ },
+ }),
+ validator(
+ "query",
+ z.object({
+ query: z.string(),
+ }),
+ ),
+ async (c) => {
+ /*
+ const query = c.req.valid("query").query
+ const result = await LSP.workspaceSymbol(query)
+ return c.json(result)
+ */
+ return c.json([])
+ },
+ )
+ .get(
+ "/file",
+ describeRoute({
+ summary: "List files",
+ description: "List files and directories in a specified path.",
+ operationId: "file.list",
+ responses: {
+ 200: {
+ description: "Files and directories",
+ content: {
+ "application/json": {
+ schema: resolver(File.Node.array()),
+ },
+ },
+ },
+ },
+ }),
+ validator(
+ "query",
+ z.object({
+ path: z.string(),
+ }),
+ ),
+ async (c) => {
+ const path = c.req.valid("query").path
+ const content = await File.list(path)
+ return c.json(content)
+ },
+ )
+ .get(
+ "/file/content",
+ describeRoute({
+ summary: "Read file",
+ description: "Read the content of a specified file.",
+ operationId: "file.read",
+ responses: {
+ 200: {
+ description: "File content",
+ content: {
+ "application/json": {
+ schema: resolver(File.Content),
+ },
+ },
+ },
+ },
+ }),
+ validator(
+ "query",
+ z.object({
+ path: z.string(),
+ }),
+ ),
+ async (c) => {
+ const path = c.req.valid("query").path
+ const content = await File.read(path)
+ return c.json(content)
+ },
+ )
+ .get(
+ "/file/status",
+ describeRoute({
+ summary: "Get file status",
+ description: "Get the git status of all files in the project.",
+ operationId: "file.status",
+ responses: {
+ 200: {
+ description: "File status",
+ content: {
+ "application/json": {
+ schema: resolver(File.Info.array()),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ const content = await File.status()
+ return c.json(content)
+ },
+ )
+)
diff --git a/packages/opencode/src/server/routes/global.ts b/packages/opencode/src/server/routes/global.ts
new file mode 100644
index 000000000..0bc9a12c2
--- /dev/null
+++ b/packages/opencode/src/server/routes/global.ts
@@ -0,0 +1,134 @@
+import { Hono } from "hono"
+import { describeRoute, resolver } from "hono-openapi"
+import { streamSSE } from "hono/streaming"
+import z from "zod"
+import { BusEvent } from "@/bus/bus-event"
+import { GlobalBus } from "@/bus/global"
+import { Instance } from "../../project/instance"
+import { Installation } from "@/installation"
+import { Log } from "../../util/log"
+import { lazy } from "../../util/lazy"
+
+const log = Log.create({ service: "server" })
+
+export const GlobalDisposedEvent = BusEvent.define("global.disposed", z.object({}))
+
+export const GlobalRoutes = lazy(() => new Hono()
+ .get(
+ "/health",
+ describeRoute({
+ summary: "Get health",
+ description: "Get health information about the OpenCode server.",
+ operationId: "global.health",
+ responses: {
+ 200: {
+ description: "Health information",
+ content: {
+ "application/json": {
+ schema: resolver(z.object({ healthy: z.literal(true), version: z.string() })),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ return c.json({ healthy: true, version: Installation.VERSION })
+ },
+ )
+ .get(
+ "/event",
+ describeRoute({
+ summary: "Get global events",
+ description: "Subscribe to global events from the OpenCode system using server-sent events.",
+ operationId: "global.event",
+ responses: {
+ 200: {
+ description: "Event stream",
+ content: {
+ "text/event-stream": {
+ schema: resolver(
+ z
+ .object({
+ directory: z.string(),
+ payload: BusEvent.payloads(),
+ })
+ .meta({
+ ref: "GlobalEvent",
+ }),
+ ),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ log.info("global event connected")
+ return streamSSE(c, async (stream) => {
+ stream.writeSSE({
+ data: JSON.stringify({
+ payload: {
+ type: "server.connected",
+ properties: {},
+ },
+ }),
+ })
+ async function handler(event: any) {
+ await stream.writeSSE({
+ data: JSON.stringify(event),
+ })
+ }
+ GlobalBus.on("event", handler)
+
+ // Send heartbeat every 30s to prevent WKWebView timeout (60s default)
+ const heartbeat = setInterval(() => {
+ stream.writeSSE({
+ data: JSON.stringify({
+ payload: {
+ type: "server.heartbeat",
+ properties: {},
+ },
+ }),
+ })
+ }, 30000)
+
+ await new Promise<void>((resolve) => {
+ stream.onAbort(() => {
+ clearInterval(heartbeat)
+ GlobalBus.off("event", handler)
+ resolve()
+ log.info("global event disconnected")
+ })
+ })
+ })
+ },
+ )
+ .post(
+ "/dispose",
+ describeRoute({
+ summary: "Dispose instance",
+ description: "Clean up and dispose all OpenCode instances, releasing all resources.",
+ operationId: "global.dispose",
+ responses: {
+ 200: {
+ description: "Global disposed",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ await Instance.disposeAll()
+ GlobalBus.emit("event", {
+ directory: "global",
+ payload: {
+ type: GlobalDisposedEvent.type,
+ properties: {},
+ },
+ })
+ return c.json(true)
+ },
+ )
+)
diff --git a/packages/opencode/src/server/routes/mcp.ts b/packages/opencode/src/server/routes/mcp.ts
new file mode 100644
index 000000000..e05b47e51
--- /dev/null
+++ b/packages/opencode/src/server/routes/mcp.ts
@@ -0,0 +1,224 @@
+import { Hono } from "hono"
+import { describeRoute, validator, resolver } from "hono-openapi"
+import z from "zod"
+import { MCP } from "../../mcp"
+import { Config } from "../../config/config"
+import { errors } from "../error"
+import { lazy } from "../../util/lazy"
+
+export const McpRoutes = lazy(() => new Hono()
+ .get(
+ "/",
+ describeRoute({
+ summary: "Get MCP status",
+ description: "Get the status of all Model Context Protocol (MCP) servers.",
+ operationId: "mcp.status",
+ responses: {
+ 200: {
+ description: "MCP server status",
+ content: {
+ "application/json": {
+ schema: resolver(z.record(z.string(), MCP.Status)),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ return c.json(await MCP.status())
+ },
+ )
+ .post(
+ "/",
+ describeRoute({
+ summary: "Add MCP server",
+ description: "Dynamically add a new Model Context Protocol (MCP) server to the system.",
+ operationId: "mcp.add",
+ responses: {
+ 200: {
+ description: "MCP server added successfully",
+ content: {
+ "application/json": {
+ schema: resolver(z.record(z.string(), MCP.Status)),
+ },
+ },
+ },
+ ...errors(400),
+ },
+ }),
+ validator(
+ "json",
+ z.object({
+ name: z.string(),
+ config: Config.Mcp,
+ }),
+ ),
+ async (c) => {
+ const { name, config } = c.req.valid("json")
+ const result = await MCP.add(name, config)
+ return c.json(result.status)
+ },
+ )
+ .post(
+ "/:name/auth",
+ describeRoute({
+ summary: "Start MCP OAuth",
+ description: "Start OAuth authentication flow for a Model Context Protocol (MCP) server.",
+ operationId: "mcp.auth.start",
+ responses: {
+ 200: {
+ description: "OAuth flow started",
+ content: {
+ "application/json": {
+ schema: resolver(
+ z.object({
+ authorizationUrl: z.string().describe("URL to open in browser for authorization"),
+ }),
+ ),
+ },
+ },
+ },
+ ...errors(400, 404),
+ },
+ }),
+ async (c) => {
+ const name = c.req.param("name")
+ const supportsOAuth = await MCP.supportsOAuth(name)
+ if (!supportsOAuth) {
+ return c.json({ error: `MCP server ${name} does not support OAuth` }, 400)
+ }
+ const result = await MCP.startAuth(name)
+ return c.json(result)
+ },
+ )
+ .post(
+ "/:name/auth/callback",
+ describeRoute({
+ summary: "Complete MCP OAuth",
+ description:
+ "Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code.",
+ operationId: "mcp.auth.callback",
+ responses: {
+ 200: {
+ description: "OAuth authentication completed",
+ content: {
+ "application/json": {
+ schema: resolver(MCP.Status),
+ },
+ },
+ },
+ ...errors(400, 404),
+ },
+ }),
+ validator(
+ "json",
+ z.object({
+ code: z.string().describe("Authorization code from OAuth callback"),
+ }),
+ ),
+ async (c) => {
+ const name = c.req.param("name")
+ const { code } = c.req.valid("json")
+ const status = await MCP.finishAuth(name, code)
+ return c.json(status)
+ },
+ )
+ .post(
+ "/:name/auth/authenticate",
+ describeRoute({
+ summary: "Authenticate MCP OAuth",
+ description: "Start OAuth flow and wait for callback (opens browser)",
+ operationId: "mcp.auth.authenticate",
+ responses: {
+ 200: {
+ description: "OAuth authentication completed",
+ content: {
+ "application/json": {
+ schema: resolver(MCP.Status),
+ },
+ },
+ },
+ ...errors(400, 404),
+ },
+ }),
+ async (c) => {
+ const name = c.req.param("name")
+ const supportsOAuth = await MCP.supportsOAuth(name)
+ if (!supportsOAuth) {
+ return c.json({ error: `MCP server ${name} does not support OAuth` }, 400)
+ }
+ const status = await MCP.authenticate(name)
+ return c.json(status)
+ },
+ )
+ .delete(
+ "/:name/auth",
+ describeRoute({
+ summary: "Remove MCP OAuth",
+ description: "Remove OAuth credentials for an MCP server",
+ operationId: "mcp.auth.remove",
+ responses: {
+ 200: {
+ description: "OAuth credentials removed",
+ content: {
+ "application/json": {
+ schema: resolver(z.object({ success: z.literal(true) })),
+ },
+ },
+ },
+ ...errors(404),
+ },
+ }),
+ async (c) => {
+ const name = c.req.param("name")
+ await MCP.removeAuth(name)
+ return c.json({ success: true as const })
+ },
+ )
+ .post(
+ "/:name/connect",
+ describeRoute({
+ description: "Connect an MCP server",
+ operationId: "mcp.connect",
+ responses: {
+ 200: {
+ description: "MCP server connected successfully",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ },
+ }),
+ validator("param", z.object({ name: z.string() })),
+ async (c) => {
+ const { name } = c.req.valid("param")
+ await MCP.connect(name)
+ return c.json(true)
+ },
+ )
+ .post(
+ "/:name/disconnect",
+ describeRoute({
+ description: "Disconnect an MCP server",
+ operationId: "mcp.disconnect",
+ responses: {
+ 200: {
+ description: "MCP server disconnected successfully",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ },
+ }),
+ validator("param", z.object({ name: z.string() })),
+ async (c) => {
+ const { name } = c.req.valid("param")
+ await MCP.disconnect(name)
+ return c.json(true)
+ },
+ )
+)
diff --git a/packages/opencode/src/server/routes/permission.ts b/packages/opencode/src/server/routes/permission.ts
new file mode 100644
index 000000000..00a79aa5c
--- /dev/null
+++ b/packages/opencode/src/server/routes/permission.ts
@@ -0,0 +1,67 @@
+import { Hono } from "hono"
+import { describeRoute, validator, resolver } from "hono-openapi"
+import z from "zod"
+import { PermissionNext } from "@/permission/next"
+import { errors } from "../error"
+import { lazy } from "../../util/lazy"
+
+export const PermissionRoutes = lazy(() => new Hono()
+ .post(
+ "/:requestID/reply",
+ describeRoute({
+ summary: "Respond to permission request",
+ description: "Approve or deny a permission request from the AI assistant.",
+ operationId: "permission.reply",
+ responses: {
+ 200: {
+ description: "Permission processed successfully",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ ...errors(400, 404),
+ },
+ }),
+ validator(
+ "param",
+ z.object({
+ requestID: z.string(),
+ }),
+ ),
+ validator("json", z.object({ reply: PermissionNext.Reply, message: z.string().optional() })),
+ async (c) => {
+ const params = c.req.valid("param")
+ const json = c.req.valid("json")
+ await PermissionNext.reply({
+ requestID: params.requestID,
+ reply: json.reply,
+ message: json.message,
+ })
+ return c.json(true)
+ },
+ )
+ .get(
+ "/",
+ describeRoute({
+ summary: "List pending permissions",
+ description: "Get all pending permission requests across all sessions.",
+ operationId: "permission.list",
+ responses: {
+ 200: {
+ description: "List of pending permissions",
+ content: {
+ "application/json": {
+ schema: resolver(PermissionNext.Request.array()),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ const permissions = await PermissionNext.list()
+ return c.json(permissions)
+ },
+ )
+)
diff --git a/packages/opencode/src/server/project.ts b/packages/opencode/src/server/routes/project.ts
index bac501803..f9cf07bd1 100644
--- a/packages/opencode/src/server/project.ts
+++ b/packages/opencode/src/server/routes/project.ts
@@ -1,12 +1,13 @@
import { Hono } from "hono"
import { describeRoute, validator } from "hono-openapi"
import { resolver } from "hono-openapi"
-import { Instance } from "../project/instance"
-import { Project } from "../project/project"
+import { Instance } from "../../project/instance"
+import { Project } from "../../project/project"
import z from "zod"
-import { errors } from "./error"
+import { errors } from "../error"
+import { lazy } from "../../util/lazy"
-export const ProjectRoute = new Hono()
+export const ProjectRoutes = lazy(() => new Hono()
.get(
"/",
describeRoute({
@@ -77,3 +78,4 @@ export const ProjectRoute = new Hono()
return c.json(project)
},
)
+)
diff --git a/packages/opencode/src/server/routes/provider.ts b/packages/opencode/src/server/routes/provider.ts
new file mode 100644
index 000000000..22a2961fe
--- /dev/null
+++ b/packages/opencode/src/server/routes/provider.ts
@@ -0,0 +1,164 @@
+import { Hono } from "hono"
+import { describeRoute, validator, resolver } from "hono-openapi"
+import z from "zod"
+import { Config } from "../../config/config"
+import { Provider } from "../../provider/provider"
+import { ModelsDev } from "../../provider/models"
+import { ProviderAuth } from "../../provider/auth"
+import { mapValues } from "remeda"
+import { errors } from "../error"
+import { lazy } from "../../util/lazy"
+
+export const ProviderRoutes = lazy(() => new Hono()
+ .get(
+ "/",
+ describeRoute({
+ summary: "List providers",
+ description: "Get a list of all available AI providers, including both available and connected ones.",
+ operationId: "provider.list",
+ responses: {
+ 200: {
+ description: "List of providers",
+ content: {
+ "application/json": {
+ schema: resolver(
+ z.object({
+ all: ModelsDev.Provider.array(),
+ default: z.record(z.string(), z.string()),
+ connected: z.array(z.string()),
+ }),
+ ),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ const config = await Config.get()
+ const disabled = new Set(config.disabled_providers ?? [])
+ const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
+
+ const allProviders = await ModelsDev.get()
+ const filteredProviders: Record<string, (typeof allProviders)[string]> = {}
+ for (const [key, value] of Object.entries(allProviders)) {
+ if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) {
+ filteredProviders[key] = value
+ }
+ }
+
+ const connected = await Provider.list()
+ const providers = Object.assign(
+ mapValues(filteredProviders, (x) => Provider.fromModelsDevProvider(x)),
+ connected,
+ )
+ return c.json({
+ all: Object.values(providers),
+ default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
+ connected: Object.keys(connected),
+ })
+ },
+ )
+ .get(
+ "/auth",
+ describeRoute({
+ summary: "Get provider auth methods",
+ description: "Retrieve available authentication methods for all AI providers.",
+ operationId: "provider.auth",
+ responses: {
+ 200: {
+ description: "Provider auth methods",
+ content: {
+ "application/json": {
+ schema: resolver(z.record(z.string(), z.array(ProviderAuth.Method))),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ return c.json(await ProviderAuth.methods())
+ },
+ )
+ .post(
+ "/:providerID/oauth/authorize",
+ describeRoute({
+ summary: "OAuth authorize",
+ description: "Initiate OAuth authorization for a specific AI provider to get an authorization URL.",
+ operationId: "provider.oauth.authorize",
+ responses: {
+ 200: {
+ description: "Authorization URL and method",
+ content: {
+ "application/json": {
+ schema: resolver(ProviderAuth.Authorization.optional()),
+ },
+ },
+ },
+ ...errors(400),
+ },
+ }),
+ validator(
+ "param",
+ z.object({
+ providerID: z.string().meta({ description: "Provider ID" }),
+ }),
+ ),
+ validator(
+ "json",
+ z.object({
+ method: z.number().meta({ description: "Auth method index" }),
+ }),
+ ),
+ async (c) => {
+ const providerID = c.req.valid("param").providerID
+ const { method } = c.req.valid("json")
+ const result = await ProviderAuth.authorize({
+ providerID,
+ method,
+ })
+ return c.json(result)
+ },
+ )
+ .post(
+ "/:providerID/oauth/callback",
+ describeRoute({
+ summary: "OAuth callback",
+ description: "Handle the OAuth callback from a provider after user authorization.",
+ operationId: "provider.oauth.callback",
+ responses: {
+ 200: {
+ description: "OAuth callback processed successfully",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ ...errors(400),
+ },
+ }),
+ validator(
+ "param",
+ z.object({
+ providerID: z.string().meta({ description: "Provider ID" }),
+ }),
+ ),
+ validator(
+ "json",
+ z.object({
+ method: z.number().meta({ description: "Auth method index" }),
+ code: z.string().optional().meta({ description: "OAuth authorization code" }),
+ }),
+ ),
+ async (c) => {
+ const providerID = c.req.valid("param").providerID
+ const { method, code } = c.req.valid("json")
+ await ProviderAuth.callback({
+ providerID,
+ method,
+ code,
+ })
+ return c.json(true)
+ },
+ )
+)
diff --git a/packages/opencode/src/server/routes/pty.ts b/packages/opencode/src/server/routes/pty.ts
new file mode 100644
index 000000000..dbe5797f6
--- /dev/null
+++ b/packages/opencode/src/server/routes/pty.ts
@@ -0,0 +1,168 @@
+import { Hono } from "hono"
+import { describeRoute, validator, resolver } from "hono-openapi"
+import { upgradeWebSocket } from "hono/bun"
+import z from "zod"
+import { Pty } from "@/pty"
+import { Storage } from "../../storage/storage"
+import { errors } from "../error"
+import { lazy } from "../../util/lazy"
+
+export const PtyRoutes = lazy(() => new Hono()
+ .get(
+ "/",
+ describeRoute({
+ summary: "List PTY sessions",
+ description: "Get a list of all active pseudo-terminal (PTY) sessions managed by OpenCode.",
+ operationId: "pty.list",
+ responses: {
+ 200: {
+ description: "List of sessions",
+ content: {
+ "application/json": {
+ schema: resolver(Pty.Info.array()),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ return c.json(Pty.list())
+ },
+ )
+ .post(
+ "/",
+ describeRoute({
+ summary: "Create PTY session",
+ description: "Create a new pseudo-terminal (PTY) session for running shell commands and processes.",
+ operationId: "pty.create",
+ responses: {
+ 200: {
+ description: "Created session",
+ content: {
+ "application/json": {
+ schema: resolver(Pty.Info),
+ },
+ },
+ },
+ ...errors(400),
+ },
+ }),
+ validator("json", Pty.CreateInput),
+ async (c) => {
+ const info = await Pty.create(c.req.valid("json"))
+ return c.json(info)
+ },
+ )
+ .get(
+ "/:ptyID",
+ describeRoute({
+ summary: "Get PTY session",
+ description: "Retrieve detailed information about a specific pseudo-terminal (PTY) session.",
+ operationId: "pty.get",
+ responses: {
+ 200: {
+ description: "Session info",
+ content: {
+ "application/json": {
+ schema: resolver(Pty.Info),
+ },
+ },
+ },
+ ...errors(404),
+ },
+ }),
+ validator("param", z.object({ ptyID: z.string() })),
+ async (c) => {
+ const info = Pty.get(c.req.valid("param").ptyID)
+ if (!info) {
+ throw new Storage.NotFoundError({ message: "Session not found" })
+ }
+ return c.json(info)
+ },
+ )
+ .put(
+ "/:ptyID",
+ describeRoute({
+ summary: "Update PTY session",
+ description: "Update properties of an existing pseudo-terminal (PTY) session.",
+ operationId: "pty.update",
+ responses: {
+ 200: {
+ description: "Updated session",
+ content: {
+ "application/json": {
+ schema: resolver(Pty.Info),
+ },
+ },
+ },
+ ...errors(400),
+ },
+ }),
+ validator("param", z.object({ ptyID: z.string() })),
+ validator("json", Pty.UpdateInput),
+ async (c) => {
+ const info = await Pty.update(c.req.valid("param").ptyID, c.req.valid("json"))
+ return c.json(info)
+ },
+ )
+ .delete(
+ "/:ptyID",
+ describeRoute({
+ summary: "Remove PTY session",
+ description: "Remove and terminate a specific pseudo-terminal (PTY) session.",
+ operationId: "pty.remove",
+ responses: {
+ 200: {
+ description: "Session removed",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ ...errors(404),
+ },
+ }),
+ validator("param", z.object({ ptyID: z.string() })),
+ async (c) => {
+ await Pty.remove(c.req.valid("param").ptyID)
+ return c.json(true)
+ },
+ )
+ .get(
+ "/:ptyID/connect",
+ describeRoute({
+ summary: "Connect to PTY session",
+ description: "Establish a WebSocket connection to interact with a pseudo-terminal (PTY) session in real-time.",
+ operationId: "pty.connect",
+ responses: {
+ 200: {
+ description: "Connected session",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ ...errors(404),
+ },
+ }),
+ validator("param", z.object({ ptyID: z.string() })),
+ upgradeWebSocket((c) => {
+ const id = c.req.param("ptyID")
+ let handler: ReturnType<typeof Pty.connect>
+ if (!Pty.get(id)) throw new Error("Session not found")
+ return {
+ onOpen(_event, ws) {
+ handler = Pty.connect(id, ws)
+ },
+ onMessage(event) {
+ handler?.onMessage(String(event.data))
+ },
+ onClose() {
+ handler?.onClose()
+ },
+ }
+ }),
+ )
+)
diff --git a/packages/opencode/src/server/question.ts b/packages/opencode/src/server/routes/question.ts
index c893862ca..4c9eac68a 100644
--- a/packages/opencode/src/server/question.ts
+++ b/packages/opencode/src/server/routes/question.ts
@@ -1,11 +1,12 @@
import { Hono } from "hono"
import { describeRoute, validator } from "hono-openapi"
import { resolver } from "hono-openapi"
-import { Question } from "../question"
+import { Question } from "../../question"
import z from "zod"
-import { errors } from "./error"
+import { errors } from "../error"
+import { lazy } from "../../util/lazy"
-export const QuestionRoute = new Hono()
+export const QuestionRoutes = lazy(() => new Hono()
.get(
"/",
describeRoute({
@@ -93,3 +94,4 @@ export const QuestionRoute = new Hono()
return c.json(true)
},
)
+)
diff --git a/packages/opencode/src/server/routes/session.ts b/packages/opencode/src/server/routes/session.ts
new file mode 100644
index 000000000..a98624dfa
--- /dev/null
+++ b/packages/opencode/src/server/routes/session.ts
@@ -0,0 +1,935 @@
+import { Hono } from "hono"
+import { stream } from "hono/streaming"
+import { describeRoute, validator, resolver } from "hono-openapi"
+import z from "zod"
+import { Session } from "../../session"
+import { MessageV2 } from "../../session/message-v2"
+import { SessionPrompt } from "../../session/prompt"
+import { SessionCompaction } from "../../session/compaction"
+import { SessionRevert } from "../../session/revert"
+import { SessionStatus } from "@/session/status"
+import { SessionSummary } from "@/session/summary"
+import { Todo } from "../../session/todo"
+import { Agent } from "../../agent/agent"
+import { Snapshot } from "@/snapshot"
+import { Log } from "../../util/log"
+import { PermissionNext } from "@/permission/next"
+import { errors } from "../error"
+import { lazy } from "../../util/lazy"
+
+const log = Log.create({ service: "server" })
+
+export const SessionRoutes = lazy(() =>
+ new Hono()
+ .get(
+ "/",
+ describeRoute({
+ summary: "List sessions",
+ description: "Get a list of all OpenCode sessions, sorted by most recently updated.",
+ operationId: "session.list",
+ responses: {
+ 200: {
+ description: "List of sessions",
+ content: {
+ "application/json": {
+ schema: resolver(Session.Info.array()),
+ },
+ },
+ },
+ },
+ }),
+ validator(
+ "query",
+ z.object({
+ directory: z.string().optional().meta({ description: "Filter sessions by project directory" }),
+ roots: z.coerce.boolean().optional().meta({ description: "Only return root sessions (no parentID)" }),
+ start: z.coerce
+ .number()
+ .optional()
+ .meta({ description: "Filter sessions updated on or after this timestamp (milliseconds since epoch)" }),
+ search: z.string().optional().meta({ description: "Filter sessions by title (case-insensitive)" }),
+ limit: z.coerce.number().optional().meta({ description: "Maximum number of sessions to return" }),
+ }),
+ ),
+ async (c) => {
+ const query = c.req.valid("query")
+ const term = query.search?.toLowerCase()
+ const sessions: Session.Info[] = []
+ for await (const session of Session.list()) {
+ if (query.directory !== undefined && session.directory !== query.directory) continue
+ if (query.roots && session.parentID) continue
+ if (query.start !== undefined && session.time.updated < query.start) continue
+ if (term !== undefined && !session.title.toLowerCase().includes(term)) continue
+ sessions.push(session)
+ if (query.limit !== undefined && sessions.length >= query.limit) break
+ }
+ return c.json(sessions)
+ },
+ )
+ .get(
+ "/status",
+ describeRoute({
+ summary: "Get session status",
+ description: "Retrieve the current status of all sessions, including active, idle, and completed states.",
+ operationId: "session.status",
+ responses: {
+ 200: {
+ description: "Get session status",
+ content: {
+ "application/json": {
+ schema: resolver(z.record(z.string(), SessionStatus.Info)),
+ },
+ },
+ },
+ ...errors(400),
+ },
+ }),
+ async (c) => {
+ const result = SessionStatus.list()
+ return c.json(result)
+ },
+ )
+ .get(
+ "/:sessionID",
+ describeRoute({
+ summary: "Get session",
+ description: "Retrieve detailed information about a specific OpenCode session.",
+ tags: ["Session"],
+ operationId: "session.get",
+ responses: {
+ 200: {
+ description: "Get session",
+ content: {
+ "application/json": {
+ schema: resolver(Session.Info),
+ },
+ },
+ },
+ ...errors(400, 404),
+ },
+ }),
+ validator(
+ "param",
+ z.object({
+ sessionID: Session.get.schema,
+ }),
+ ),
+ async (c) => {
+ const sessionID = c.req.valid("param").sessionID
+ log.info("SEARCH", { url: c.req.url })
+ const session = await Session.get(sessionID)
+ return c.json(session)
+ },
+ )
+ .get(
+ "/:sessionID/children",
+ describeRoute({
+ summary: "Get session children",
+ tags: ["Session"],
+ description: "Retrieve all child sessions that were forked from the specified parent session.",
+ operationId: "session.children",
+ responses: {
+ 200: {
+ description: "List of children",
+ content: {
+ "application/json": {
+ schema: resolver(Session.Info.array()),
+ },
+ },
+ },
+ ...errors(400, 404),
+ },
+ }),
+ validator(
+ "param",
+ z.object({
+ sessionID: Session.children.schema,
+ }),
+ ),
+ async (c) => {
+ const sessionID = c.req.valid("param").sessionID
+ const session = await Session.children(sessionID)
+ return c.json(session)
+ },
+ )
+ .get(
+ "/:sessionID/todo",
+ describeRoute({
+ summary: "Get session todos",
+ description: "Retrieve the todo list associated with a specific session, showing tasks and action items.",
+ operationId: "session.todo",
+ responses: {
+ 200: {
+ description: "Todo list",
+ content: {
+ "application/json": {
+ schema: resolver(Todo.Info.array()),
+ },
+ },
+ },
+ ...errors(400, 404),
+ },
+ }),
+ validator(
+ "param",
+ z.object({
+ sessionID: z.string().meta({ description: "Session ID" }),
+ }),
+ ),
+ async (c) => {
+ const sessionID = c.req.valid("param").sessionID
+ const todos = await Todo.get(sessionID)
+ return c.json(todos)
+ },
+ )
+ .post(
+ "/",
+ describeRoute({
+ summary: "Create session",
+ description: "Create a new OpenCode session for interacting with AI assistants and managing conversations.",
+ operationId: "session.create",
+ responses: {
+ ...errors(400),
+ 200: {
+ description: "Successfully created session",
+ content: {
+ "application/json": {
+ schema: resolver(Session.Info),
+ },
+ },
+ },
+ },
+ }),
+ validator("json", Session.create.schema.optional()),
+ async (c) => {
+ const body = c.req.valid("json") ?? {}
+ const session = await Session.create(body)
+ return c.json(session)
+ },
+ )
+ .delete(
+ "/:sessionID",
+ describeRoute({
+ summary: "Delete session",
+ description: "Delete a session and permanently remove all associated data, including messages and history.",
+ operationId: "session.delete",
+ responses: {
+ 200: {
+ description: "Successfully deleted session",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ ...errors(400, 404),
+ },
+ }),
+ validator(
+ "param",
+ z.object({
+ sessionID: Session.remove.schema,
+ }),
+ ),
+ async (c) => {
+ const sessionID = c.req.valid("param").sessionID
+ await Session.remove(sessionID)
+ return c.json(true)
+ },
+ )
+ .patch(
+ "/:sessionID",
+ describeRoute({
+ summary: "Update session",
+ description: "Update properties of an existing session, such as title or other metadata.",
+ operationId: "session.update",
+ responses: {
+ 200: {
+ description: "Successfully updated session",
+ content: {
+ "application/json": {
+ schema: resolver(Session.Info),
+ },
+ },
+ },
+ ...errors(400, 404),
+ },
+ }),
+ validator(
+ "param",
+ z.object({
+ sessionID: z.string(),
+ }),
+ ),
+ validator(
+ "json",
+ z.object({
+ title: z.string().optional(),
+ time: z
+ .object({
+ archived: z.number().optional(),
+ })
+ .optional(),
+ }),
+ ),
+ async (c) => {
+ const sessionID = c.req.valid("param").sessionID
+ const updates = c.req.valid("json")
+
+ const updatedSession = await Session.update(sessionID, (session) => {
+ if (updates.title !== undefined) {
+ session.title = updates.title
+ }
+ if (updates.time?.archived !== undefined) session.time.archived = updates.time.archived
+ })
+
+ return c.json(updatedSession)
+ },
+ )
+ .post(
+ "/:sessionID/init",
+ describeRoute({
+ summary: "Initialize session",
+ description:
+ "Analyze the current application and create an AGENTS.md file with project-specific agent configurations.",
+ operationId: "session.init",
+ responses: {
+ 200: {
+ description: "200",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ ...errors(400, 404),
+ },
+ }),
+ validator(
+ "param",
+ z.object({
+ sessionID: z.string().meta({ description: "Session ID" }),
+ }),
+ ),
+ validator("json", Session.initialize.schema.omit({ sessionID: true })),
+ async (c) => {
+ const sessionID = c.req.valid("param").sessionID
+ const body = c.req.valid("json")
+ await Session.initialize({ ...body, sessionID })
+ return c.json(true)
+ },
+ )
+ .post(
+ "/:sessionID/fork",
+ describeRoute({
+ summary: "Fork session",
+ description: "Create a new session by forking an existing session at a specific message point.",
+ operationId: "session.fork",
+ responses: {
+ 200: {
+ description: "200",
+ content: {
+ "application/json": {
+ schema: resolver(Session.Info),
+ },
+ },
+ },
+ },
+ }),
+ validator(
+ "param",
+ z.object({
+ sessionID: Session.fork.schema.shape.sessionID,
+ }),
+ ),
+ validator("json", Session.fork.schema.omit({ sessionID: true })),
+ async (c) => {
+ const sessionID = c.req.valid("param").sessionID
+ const body = c.req.valid("json")
+ const result = await Session.fork({ ...body, sessionID })
+ return c.json(result)
+ },
+ )
+ .post(
+ "/:sessionID/abort",
+ describeRoute({
+ summary: "Abort session",
+ description: "Abort an active session and stop any ongoing AI processing or command execution.",
+ operationId: "session.abort",
+ responses: {
+ 200: {
+ description: "Aborted session",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ ...errors(400, 404),
+ },
+ }),
+ validator(
+ "param",
+ z.object({
+ sessionID: z.string(),
+ }),
+ ),
+ async (c) => {
+ SessionPrompt.cancel(c.req.valid("param").sessionID)
+ return c.json(true)
+ },
+ )
+ .post(
+ "/:sessionID/share",
+ describeRoute({
+ summary: "Share session",
+ description: "Create a shareable link for a session, allowing others to view the conversation.",
+ operationId: "session.share",
+ responses: {
+ 200: {
+ description: "Successfully shared session",
+ content: {
+ "application/json": {
+ schema: resolver(Session.Info),
+ },
+ },
+ },
+ ...errors(400, 404),
+ },
+ }),
+ validator(
+ "param",
+ z.object({
+ sessionID: z.string(),
+ }),
+ ),
+ async (c) => {
+ const sessionID = c.req.valid("param").sessionID
+ await Session.share(sessionID)
+ const session = await Session.get(sessionID)
+ return c.json(session)
+ },
+ )
+ .get(
+ "/:sessionID/diff",
+ describeRoute({
+ summary: "Get message diff",
+ description: "Get the file changes (diff) that resulted from a specific user message in the session.",
+ operationId: "session.diff",
+ responses: {
+ 200: {
+ description: "Successfully retrieved diff",
+ content: {
+ "application/json": {
+ schema: resolver(Snapshot.FileDiff.array()),
+ },
+ },
+ },
+ },
+ }),
+ validator(
+ "param",
+ z.object({
+ sessionID: SessionSummary.diff.schema.shape.sessionID,
+ }),
+ ),
+ validator(
+ "query",
+ z.object({
+ messageID: SessionSummary.diff.schema.shape.messageID,
+ }),
+ ),
+ async (c) => {
+ const query = c.req.valid("query")
+ const params = c.req.valid("param")
+ const result = await SessionSummary.diff({
+ sessionID: params.sessionID,
+ messageID: query.messageID,
+ })
+ return c.json(result)
+ },
+ )
+ .delete(
+ "/:sessionID/share",
+ describeRoute({
+ summary: "Unshare session",
+ description: "Remove the shareable link for a session, making it private again.",
+ operationId: "session.unshare",
+ responses: {
+ 200: {
+ description: "Successfully unshared session",
+ content: {
+ "application/json": {
+ schema: resolver(Session.Info),
+ },
+ },
+ },
+ ...errors(400, 404),
+ },
+ }),
+ validator(
+ "param",
+ z.object({
+ sessionID: Session.unshare.schema,
+ }),
+ ),
+ async (c) => {
+ const sessionID = c.req.valid("param").sessionID
+ await Session.unshare(sessionID)
+ const session = await Session.get(sessionID)
+ return c.json(session)
+ },
+ )
+ .post(
+ "/:sessionID/summarize",
+ describeRoute({
+ summary: "Summarize session",
+ description: "Generate a concise summary of the session using AI compaction to preserve key information.",
+ operationId: "session.summarize",
+ responses: {
+ 200: {
+ description: "Summarized session",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ ...errors(400, 404),
+ },
+ }),
+ validator(
+ "param",
+ z.object({
+ sessionID: z.string().meta({ description: "Session ID" }),
+ }),
+ ),
+ validator(
+ "json",
+ z.object({
+ providerID: z.string(),
+ modelID: z.string(),
+ auto: z.boolean().optional().default(false),
+ }),
+ ),
+ async (c) => {
+ const sessionID = c.req.valid("param").sessionID
+ const body = c.req.valid("json")
+ const session = await Session.get(sessionID)
+ await SessionRevert.cleanup(session)
+ const msgs = await Session.messages({ sessionID })
+ let currentAgent = await Agent.defaultAgent()
+ for (let i = msgs.length - 1; i >= 0; i--) {
+ const info = msgs[i].info
+ if (info.role === "user") {
+ currentAgent = info.agent || (await Agent.defaultAgent())
+ break
+ }
+ }
+ await SessionCompaction.create({
+ sessionID,
+ agent: currentAgent,
+ model: {
+ providerID: body.providerID,
+ modelID: body.modelID,
+ },
+ auto: body.auto,
+ })
+ await SessionPrompt.loop(sessionID)
+ return c.json(true)
+ },
+ )
+ .get(
+ "/:sessionID/message",
+ describeRoute({
+ summary: "Get session messages",
+ description: "Retrieve all messages in a session, including user prompts and AI responses.",
+ operationId: "session.messages",
+ responses: {
+ 200: {
+ description: "List of messages",
+ content: {
+ "application/json": {
+ schema: resolver(MessageV2.WithParts.array()),
+ },
+ },
+ },
+ ...errors(400, 404),
+ },
+ }),
+ validator(
+ "param",
+ z.object({
+ sessionID: z.string().meta({ description: "Session ID" }),
+ }),
+ ),
+ validator(
+ "query",
+ z.object({
+ limit: z.coerce.number().optional(),
+ }),
+ ),
+ async (c) => {
+ const query = c.req.valid("query")
+ const messages = await Session.messages({
+ sessionID: c.req.valid("param").sessionID,
+ limit: query.limit,
+ })
+ return c.json(messages)
+ },
+ )
+ .get(
+ "/:sessionID/message/:messageID",
+ describeRoute({
+ summary: "Get message",
+ description: "Retrieve a specific message from a session by its message ID.",
+ operationId: "session.message",
+ responses: {
+ 200: {
+ description: "Message",
+ content: {
+ "application/json": {
+ schema: resolver(
+ z.object({
+ info: MessageV2.Info,
+ parts: MessageV2.Part.array(),
+ }),
+ ),
+ },
+ },
+ },
+ ...errors(400, 404),
+ },
+ }),
+ validator(
+ "param",
+ z.object({
+ sessionID: z.string().meta({ description: "Session ID" }),
+ messageID: z.string().meta({ description: "Message ID" }),
+ }),
+ ),
+ async (c) => {
+ const params = c.req.valid("param")
+ const message = await MessageV2.get({
+ sessionID: params.sessionID,
+ messageID: params.messageID,
+ })
+ return c.json(message)
+ },
+ )
+ .delete(
+ "/:sessionID/message/:messageID/part/:partID",
+ describeRoute({
+ description: "Delete a part from a message",
+ operationId: "part.delete",
+ responses: {
+ 200: {
+ description: "Successfully deleted part",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ ...errors(400, 404),
+ },
+ }),
+ validator(
+ "param",
+ z.object({
+ sessionID: z.string().meta({ description: "Session ID" }),
+ messageID: z.string().meta({ description: "Message ID" }),
+ partID: z.string().meta({ description: "Part ID" }),
+ }),
+ ),
+ async (c) => {
+ const params = c.req.valid("param")
+ await Session.removePart({
+ sessionID: params.sessionID,
+ messageID: params.messageID,
+ partID: params.partID,
+ })
+ return c.json(true)
+ },
+ )
+ .patch(
+ "/:sessionID/message/:messageID/part/:partID",
+ describeRoute({
+ description: "Update a part in a message",
+ operationId: "part.update",
+ responses: {
+ 200: {
+ description: "Successfully updated part",
+ content: {
+ "application/json": {
+ schema: resolver(MessageV2.Part),
+ },
+ },
+ },
+ ...errors(400, 404),
+ },
+ }),
+ validator(
+ "param",
+ z.object({
+ sessionID: z.string().meta({ description: "Session ID" }),
+ messageID: z.string().meta({ description: "Message ID" }),
+ partID: z.string().meta({ description: "Part ID" }),
+ }),
+ ),
+ validator("json", MessageV2.Part),
+ async (c) => {
+ const params = c.req.valid("param")
+ const body = c.req.valid("json")
+ if (body.id !== params.partID || body.messageID !== params.messageID || body.sessionID !== params.sessionID) {
+ throw new Error(
+ `Part mismatch: body.id='${body.id}' vs partID='${params.partID}', body.messageID='${body.messageID}' vs messageID='${params.messageID}', body.sessionID='${body.sessionID}' vs sessionID='${params.sessionID}'`,
+ )
+ }
+ const part = await Session.updatePart(body)
+ return c.json(part)
+ },
+ )
+ .post(
+ "/:sessionID/message",
+ describeRoute({
+ summary: "Send message",
+ description: "Create and send a new message to a session, streaming the AI response.",
+ operationId: "session.prompt",
+ responses: {
+ 200: {
+ description: "Created message",
+ content: {
+ "application/json": {
+ schema: resolver(
+ z.object({
+ info: MessageV2.Assistant,
+ parts: MessageV2.Part.array(),
+ }),
+ ),
+ },
+ },
+ },
+ ...errors(400, 404),
+ },
+ }),
+ validator(
+ "param",
+ z.object({
+ sessionID: z.string().meta({ description: "Session ID" }),
+ }),
+ ),
+ validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
+ async (c) => {
+ c.status(200)
+ c.header("Content-Type", "application/json")
+ return stream(c, async (stream) => {
+ const sessionID = c.req.valid("param").sessionID
+ const body = c.req.valid("json")
+ const msg = await SessionPrompt.prompt({ ...body, sessionID })
+ stream.write(JSON.stringify(msg))
+ })
+ },
+ )
+ .post(
+ "/:sessionID/prompt_async",
+ describeRoute({
+ summary: "Send async message",
+ description:
+ "Create and send a new message to a session asynchronously, starting the session if needed and returning immediately.",
+ operationId: "session.prompt_async",
+ responses: {
+ 204: {
+ description: "Prompt accepted",
+ },
+ ...errors(400, 404),
+ },
+ }),
+ validator(
+ "param",
+ z.object({
+ sessionID: z.string().meta({ description: "Session ID" }),
+ }),
+ ),
+ validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
+ async (c) => {
+ c.status(204)
+ c.header("Content-Type", "application/json")
+ return stream(c, async () => {
+ const sessionID = c.req.valid("param").sessionID
+ const body = c.req.valid("json")
+ SessionPrompt.prompt({ ...body, sessionID })
+ })
+ },
+ )
+ .post(
+ "/:sessionID/command",
+ describeRoute({
+ summary: "Send command",
+ description: "Send a new command to a session for execution by the AI assistant.",
+ operationId: "session.command",
+ responses: {
+ 200: {
+ description: "Created message",
+ content: {
+ "application/json": {
+ schema: resolver(
+ z.object({
+ info: MessageV2.Assistant,
+ parts: MessageV2.Part.array(),
+ }),
+ ),
+ },
+ },
+ },
+ ...errors(400, 404),
+ },
+ }),
+ validator(
+ "param",
+ z.object({
+ sessionID: z.string().meta({ description: "Session ID" }),
+ }),
+ ),
+ validator("json", SessionPrompt.CommandInput.omit({ sessionID: true })),
+ async (c) => {
+ const sessionID = c.req.valid("param").sessionID
+ const body = c.req.valid("json")
+ const msg = await SessionPrompt.command({ ...body, sessionID })
+ return c.json(msg)
+ },
+ )
+ .post(
+ "/:sessionID/shell",
+ describeRoute({
+ summary: "Run shell command",
+ description: "Execute a shell command within the session context and return the AI's response.",
+ operationId: "session.shell",
+ responses: {
+ 200: {
+ description: "Created message",
+ content: {
+ "application/json": {
+ schema: resolver(MessageV2.Assistant),
+ },
+ },
+ },
+ ...errors(400, 404),
+ },
+ }),
+ validator(
+ "param",
+ z.object({
+ sessionID: z.string().meta({ description: "Session ID" }),
+ }),
+ ),
+ validator("json", SessionPrompt.ShellInput.omit({ sessionID: true })),
+ async (c) => {
+ const sessionID = c.req.valid("param").sessionID
+ const body = c.req.valid("json")
+ const msg = await SessionPrompt.shell({ ...body, sessionID })
+ return c.json(msg)
+ },
+ )
+ .post(
+ "/:sessionID/revert",
+ describeRoute({
+ summary: "Revert message",
+ description: "Revert a specific message in a session, undoing its effects and restoring the previous state.",
+ operationId: "session.revert",
+ responses: {
+ 200: {
+ description: "Updated session",
+ content: {
+ "application/json": {
+ schema: resolver(Session.Info),
+ },
+ },
+ },
+ ...errors(400, 404),
+ },
+ }),
+ validator(
+ "param",
+ z.object({
+ sessionID: z.string(),
+ }),
+ ),
+ validator("json", SessionRevert.RevertInput.omit({ sessionID: true })),
+ async (c) => {
+ const sessionID = c.req.valid("param").sessionID
+ log.info("revert", c.req.valid("json"))
+ const session = await SessionRevert.revert({
+ sessionID,
+ ...c.req.valid("json"),
+ })
+ return c.json(session)
+ },
+ )
+ .post(
+ "/:sessionID/unrevert",
+ describeRoute({
+ summary: "Restore reverted messages",
+ description: "Restore all previously reverted messages in a session.",
+ operationId: "session.unrevert",
+ responses: {
+ 200: {
+ description: "Updated session",
+ content: {
+ "application/json": {
+ schema: resolver(Session.Info),
+ },
+ },
+ },
+ ...errors(400, 404),
+ },
+ }),
+ validator(
+ "param",
+ z.object({
+ sessionID: z.string(),
+ }),
+ ),
+ async (c) => {
+ const sessionID = c.req.valid("param").sessionID
+ const session = await SessionRevert.unrevert({ sessionID })
+ return c.json(session)
+ },
+ )
+ .post(
+ "/:sessionID/permissions/:permissionID",
+ describeRoute({
+ summary: "Respond to permission",
+ deprecated: true,
+ description: "Approve or deny a permission request from the AI assistant.",
+ operationId: "permission.respond",
+ responses: {
+ 200: {
+ description: "Permission processed successfully",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ ...errors(400, 404),
+ },
+ }),
+ validator(
+ "param",
+ z.object({
+ sessionID: z.string(),
+ permissionID: z.string(),
+ }),
+ ),
+ validator("json", z.object({ response: PermissionNext.Reply })),
+ async (c) => {
+ const params = c.req.valid("param")
+ PermissionNext.reply({
+ requestID: params.permissionID,
+ reply: c.req.valid("json").response,
+ })
+ return c.json(true)
+ },
+ ),
+)
diff --git a/packages/opencode/src/server/routes/tui.ts b/packages/opencode/src/server/routes/tui.ts
new file mode 100644
index 000000000..be371c1e0
--- /dev/null
+++ b/packages/opencode/src/server/routes/tui.ts
@@ -0,0 +1,375 @@
+import { Hono, type Context } from "hono"
+import { describeRoute, validator, resolver } from "hono-openapi"
+import z from "zod"
+import { Bus } from "../../bus"
+import { Session } from "../../session"
+import { TuiEvent } from "@/cli/cmd/tui/event"
+import { AsyncQueue } from "../../util/queue"
+import { errors } from "../error"
+import { lazy } from "../../util/lazy"
+
+const TuiRequest = z.object({
+ path: z.string(),
+ body: z.any(),
+})
+
+type TuiRequest = z.infer<typeof TuiRequest>
+
+const request = new AsyncQueue<TuiRequest>()
+const response = new AsyncQueue<any>()
+
+export async function callTui(ctx: Context) {
+ const body = await ctx.req.json()
+ request.push({
+ path: ctx.req.path,
+ body,
+ })
+ return response.next()
+}
+
+const TuiControlRoutes = new Hono()
+ .get(
+ "/next",
+ describeRoute({
+ summary: "Get next TUI request",
+ description: "Retrieve the next TUI (Terminal User Interface) request from the queue for processing.",
+ operationId: "tui.control.next",
+ responses: {
+ 200: {
+ description: "Next TUI request",
+ content: {
+ "application/json": {
+ schema: resolver(TuiRequest),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ const req = await request.next()
+ return c.json(req)
+ },
+ )
+ .post(
+ "/response",
+ describeRoute({
+ summary: "Submit TUI response",
+ description: "Submit a response to the TUI request queue to complete a pending request.",
+ operationId: "tui.control.response",
+ responses: {
+ 200: {
+ description: "Response submitted successfully",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ },
+ }),
+ validator("json", z.any()),
+ async (c) => {
+ const body = c.req.valid("json")
+ response.push(body)
+ return c.json(true)
+ },
+ )
+
+export const TuiRoutes = lazy(() =>
+ new Hono()
+ .post(
+ "/append-prompt",
+ describeRoute({
+ summary: "Append TUI prompt",
+ description: "Append prompt to the TUI",
+ operationId: "tui.appendPrompt",
+ responses: {
+ 200: {
+ description: "Prompt processed successfully",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ ...errors(400),
+ },
+ }),
+ validator("json", TuiEvent.PromptAppend.properties),
+ async (c) => {
+ await Bus.publish(TuiEvent.PromptAppend, c.req.valid("json"))
+ return c.json(true)
+ },
+ )
+ .post(
+ "/open-help",
+ describeRoute({
+ summary: "Open help dialog",
+ description: "Open the help dialog in the TUI to display user assistance information.",
+ operationId: "tui.openHelp",
+ responses: {
+ 200: {
+ description: "Help dialog opened successfully",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ // TODO: open dialog
+ return c.json(true)
+ },
+ )
+ .post(
+ "/open-sessions",
+ describeRoute({
+ summary: "Open sessions dialog",
+ description: "Open the session dialog",
+ operationId: "tui.openSessions",
+ responses: {
+ 200: {
+ description: "Session dialog opened successfully",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ await Bus.publish(TuiEvent.CommandExecute, {
+ command: "session.list",
+ })
+ return c.json(true)
+ },
+ )
+ .post(
+ "/open-themes",
+ describeRoute({
+ summary: "Open themes dialog",
+ description: "Open the theme dialog",
+ operationId: "tui.openThemes",
+ responses: {
+ 200: {
+ description: "Theme dialog opened successfully",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ await Bus.publish(TuiEvent.CommandExecute, {
+ command: "session.list",
+ })
+ return c.json(true)
+ },
+ )
+ .post(
+ "/open-models",
+ describeRoute({
+ summary: "Open models dialog",
+ description: "Open the model dialog",
+ operationId: "tui.openModels",
+ responses: {
+ 200: {
+ description: "Model dialog opened successfully",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ await Bus.publish(TuiEvent.CommandExecute, {
+ command: "model.list",
+ })
+ return c.json(true)
+ },
+ )
+ .post(
+ "/submit-prompt",
+ describeRoute({
+ summary: "Submit TUI prompt",
+ description: "Submit the prompt",
+ operationId: "tui.submitPrompt",
+ responses: {
+ 200: {
+ description: "Prompt submitted successfully",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ await Bus.publish(TuiEvent.CommandExecute, {
+ command: "prompt.submit",
+ })
+ return c.json(true)
+ },
+ )
+ .post(
+ "/clear-prompt",
+ describeRoute({
+ summary: "Clear TUI prompt",
+ description: "Clear the prompt",
+ operationId: "tui.clearPrompt",
+ responses: {
+ 200: {
+ description: "Prompt cleared successfully",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ await Bus.publish(TuiEvent.CommandExecute, {
+ command: "prompt.clear",
+ })
+ return c.json(true)
+ },
+ )
+ .post(
+ "/execute-command",
+ describeRoute({
+ summary: "Execute TUI command",
+ description: "Execute a TUI command (e.g. agent_cycle)",
+ operationId: "tui.executeCommand",
+ responses: {
+ 200: {
+ description: "Command executed successfully",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ ...errors(400),
+ },
+ }),
+ validator("json", z.object({ command: z.string() })),
+ async (c) => {
+ const command = c.req.valid("json").command
+ await Bus.publish(TuiEvent.CommandExecute, {
+ // @ts-expect-error
+ command: {
+ session_new: "session.new",
+ session_share: "session.share",
+ session_interrupt: "session.interrupt",
+ session_compact: "session.compact",
+ messages_page_up: "session.page.up",
+ messages_page_down: "session.page.down",
+ messages_half_page_up: "session.half.page.up",
+ messages_half_page_down: "session.half.page.down",
+ messages_first: "session.first",
+ messages_last: "session.last",
+ agent_cycle: "agent.cycle",
+ }[command],
+ })
+ return c.json(true)
+ },
+ )
+ .post(
+ "/show-toast",
+ describeRoute({
+ summary: "Show TUI toast",
+ description: "Show a toast notification in the TUI",
+ operationId: "tui.showToast",
+ responses: {
+ 200: {
+ description: "Toast notification shown successfully",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ },
+ }),
+ validator("json", TuiEvent.ToastShow.properties),
+ async (c) => {
+ await Bus.publish(TuiEvent.ToastShow, c.req.valid("json"))
+ return c.json(true)
+ },
+ )
+ .post(
+ "/publish",
+ describeRoute({
+ summary: "Publish TUI event",
+ description: "Publish a TUI event",
+ operationId: "tui.publish",
+ responses: {
+ 200: {
+ description: "Event published successfully",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ ...errors(400),
+ },
+ }),
+ validator(
+ "json",
+ z.union(
+ Object.values(TuiEvent).map((def) => {
+ return z
+ .object({
+ type: z.literal(def.type),
+ properties: def.properties,
+ })
+ .meta({
+ ref: "Event" + "." + def.type,
+ })
+ }),
+ ),
+ ),
+ async (c) => {
+ const evt = c.req.valid("json")
+ await Bus.publish(Object.values(TuiEvent).find((def) => def.type === evt.type)!, evt.properties)
+ return c.json(true)
+ },
+ )
+ .post(
+ "/select-session",
+ describeRoute({
+ summary: "Select session",
+ description: "Navigate the TUI to display the specified session.",
+ operationId: "tui.selectSession",
+ responses: {
+ 200: {
+ description: "Session selected successfully",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ ...errors(400, 404),
+ },
+ }),
+ validator("json", TuiEvent.SessionSelect.properties),
+ async (c) => {
+ const { sessionID } = c.req.valid("json")
+ await Session.get(sessionID)
+ await Bus.publish(TuiEvent.SessionSelect, { sessionID })
+ return c.json(true)
+ },
+ )
+ .route("/control", TuiControlRoutes),
+)
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index 7015c8188..f0c64b49f 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -1,60 +1,45 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
-import { GlobalBus } from "@/bus/global"
import { Log } from "../util/log"
import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi"
import { Hono } from "hono"
import { cors } from "hono/cors"
-import { stream, streamSSE } from "hono/streaming"
+import { streamSSE } from "hono/streaming"
import { proxy } from "hono/proxy"
import { basicAuth } from "hono/basic-auth"
-import { Session } from "../session"
import z from "zod"
import { Provider } from "../provider/provider"
-import { filter, mapValues, sortBy, pipe } from "remeda"
import { NamedError } from "@opencode-ai/util/error"
-import { ModelsDev } from "../provider/models"
-import { Ripgrep } from "../file/ripgrep"
-import { Config } from "../config/config"
-import { File } from "../file"
import { LSP } from "../lsp"
import { Format } from "../format"
-import { MessageV2 } from "../session/message-v2"
-import { TuiRoute } from "./tui"
+import { TuiRoutes } from "./routes/tui"
import { Instance } from "../project/instance"
-import { Project } from "../project/project"
import { Vcs } from "../project/vcs"
import { Agent } from "../agent/agent"
+import { Skill } from "../skill/skill"
import { Auth } from "../auth"
import { Flag } from "../flag/flag"
import { Command } from "../command"
-import { ProviderAuth } from "../provider/auth"
import { Global } from "../global"
-import { ProjectRoute } from "./project"
-import { ToolRegistry } from "../tool/registry"
-import { zodToJsonSchema } from "zod-to-json-schema"
-import { SessionPrompt } from "../session/prompt"
-import { SessionCompaction } from "../session/compaction"
-import { SessionRevert } from "../session/revert"
+import { ProjectRoutes } from "./routes/project"
+import { SessionRoutes } from "./routes/session"
+import { PtyRoutes } from "./routes/pty"
+import { McpRoutes } from "./routes/mcp"
+import { FileRoutes } from "./routes/file"
+import { ConfigRoutes } from "./routes/config"
+import { ExperimentalRoutes } from "./routes/experimental"
+import { ProviderRoutes } from "./routes/provider"
import { lazy } from "../util/lazy"
-import { Todo } from "../session/todo"
import { InstanceBootstrap } from "../project/bootstrap"
-import { MCP } from "../mcp"
import { Storage } from "../storage/storage"
import type { ContentfulStatusCode } from "hono/utils/http-status"
-import { TuiEvent } from "@/cli/cmd/tui/event"
-import { Snapshot } from "@/snapshot"
-import { SessionSummary } from "@/session/summary"
-import { SessionStatus } from "@/session/status"
-import { upgradeWebSocket, websocket } from "hono/bun"
+import { websocket } from "hono/bun"
import { HTTPException } from "hono/http-exception"
import { errors } from "./error"
-import { Pty } from "@/pty"
-import { PermissionNext } from "@/permission/next"
-import { QuestionRoute } from "./question"
-import { Installation } from "@/installation"
+import { QuestionRoutes } from "./routes/question"
+import { PermissionRoutes } from "./routes/permission"
+import { GlobalRoutes } from "./routes/global"
import { MDNS } from "./mdns"
-import { Worktree } from "../worktree"
// @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
globalThis.AI_SDK_LOG_WARNINGS = false
@@ -141,123 +126,7 @@ export namespace Server {
},
}),
)
- .get(
- "/global/health",
- describeRoute({
- summary: "Get health",
- description: "Get health information about the OpenCode server.",
- operationId: "global.health",
- responses: {
- 200: {
- description: "Health information",
- content: {
- "application/json": {
- schema: resolver(z.object({ healthy: z.literal(true), version: z.string() })),
- },
- },
- },
- },
- }),
- async (c) => {
- return c.json({ healthy: true, version: Installation.VERSION })
- },
- )
- .get(
- "/global/event",
- describeRoute({
- summary: "Get global events",
- description: "Subscribe to global events from the OpenCode system using server-sent events.",
- operationId: "global.event",
- responses: {
- 200: {
- description: "Event stream",
- content: {
- "text/event-stream": {
- schema: resolver(
- z
- .object({
- directory: z.string(),
- payload: BusEvent.payloads(),
- })
- .meta({
- ref: "GlobalEvent",
- }),
- ),
- },
- },
- },
- },
- }),
- async (c) => {
- log.info("global event connected")
- return streamSSE(c, async (stream) => {
- stream.writeSSE({
- data: JSON.stringify({
- payload: {
- type: "server.connected",
- properties: {},
- },
- }),
- })
- async function handler(event: any) {
- await stream.writeSSE({
- data: JSON.stringify(event),
- })
- }
- GlobalBus.on("event", handler)
-
- // Send heartbeat every 30s to prevent WKWebView timeout (60s default)
- const heartbeat = setInterval(() => {
- stream.writeSSE({
- data: JSON.stringify({
- payload: {
- type: "server.heartbeat",
- properties: {},
- },
- }),
- })
- }, 30000)
-
- await new Promise<void>((resolve) => {
- stream.onAbort(() => {
- clearInterval(heartbeat)
- GlobalBus.off("event", handler)
- resolve()
- log.info("global event disconnected")
- })
- })
- })
- },
- )
- .post(
- "/global/dispose",
- describeRoute({
- summary: "Dispose instance",
- description: "Clean up and dispose all OpenCode instances, releasing all resources.",
- operationId: "global.dispose",
- responses: {
- 200: {
- description: "Global disposed",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- },
- }),
- async (c) => {
- await Instance.disposeAll()
- GlobalBus.emit("event", {
- directory: "global",
- payload: {
- type: Event.Disposed.type,
- properties: {},
- },
- })
- return c.json(true)
- },
- )
+ .route("/global", GlobalRoutes())
.use(async (c, next) => {
let directory = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
try {
@@ -287,289 +156,17 @@ export namespace Server {
}),
)
.use(validator("query", z.object({ directory: z.string().optional() })))
-
- .route("/project", ProjectRoute)
-
- .get(
- "/pty",
- describeRoute({
- summary: "List PTY sessions",
- description: "Get a list of all active pseudo-terminal (PTY) sessions managed by OpenCode.",
- operationId: "pty.list",
- responses: {
- 200: {
- description: "List of sessions",
- content: {
- "application/json": {
- schema: resolver(Pty.Info.array()),
- },
- },
- },
- },
- }),
- async (c) => {
- return c.json(Pty.list())
- },
- )
- .post(
- "/pty",
- describeRoute({
- summary: "Create PTY session",
- description: "Create a new pseudo-terminal (PTY) session for running shell commands and processes.",
- operationId: "pty.create",
- responses: {
- 200: {
- description: "Created session",
- content: {
- "application/json": {
- schema: resolver(Pty.Info),
- },
- },
- },
- ...errors(400),
- },
- }),
- validator("json", Pty.CreateInput),
- async (c) => {
- const info = await Pty.create(c.req.valid("json"))
- return c.json(info)
- },
- )
- .get(
- "/pty/:ptyID",
- describeRoute({
- summary: "Get PTY session",
- description: "Retrieve detailed information about a specific pseudo-terminal (PTY) session.",
- operationId: "pty.get",
- responses: {
- 200: {
- description: "Session info",
- content: {
- "application/json": {
- schema: resolver(Pty.Info),
- },
- },
- },
- ...errors(404),
- },
- }),
- validator("param", z.object({ ptyID: z.string() })),
- async (c) => {
- const info = Pty.get(c.req.valid("param").ptyID)
- if (!info) {
- throw new Storage.NotFoundError({ message: "Session not found" })
- }
- return c.json(info)
- },
- )
- .put(
- "/pty/:ptyID",
- describeRoute({
- summary: "Update PTY session",
- description: "Update properties of an existing pseudo-terminal (PTY) session.",
- operationId: "pty.update",
- responses: {
- 200: {
- description: "Updated session",
- content: {
- "application/json": {
- schema: resolver(Pty.Info),
- },
- },
- },
- ...errors(400),
- },
- }),
- validator("param", z.object({ ptyID: z.string() })),
- validator("json", Pty.UpdateInput),
- async (c) => {
- const info = await Pty.update(c.req.valid("param").ptyID, c.req.valid("json"))
- return c.json(info)
- },
- )
- .delete(
- "/pty/:ptyID",
- describeRoute({
- summary: "Remove PTY session",
- description: "Remove and terminate a specific pseudo-terminal (PTY) session.",
- operationId: "pty.remove",
- responses: {
- 200: {
- description: "Session removed",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- ...errors(404),
- },
- }),
- validator("param", z.object({ ptyID: z.string() })),
- async (c) => {
- await Pty.remove(c.req.valid("param").ptyID)
- return c.json(true)
- },
- )
- .get(
- "/pty/:ptyID/connect",
- describeRoute({
- summary: "Connect to PTY session",
- description:
- "Establish a WebSocket connection to interact with a pseudo-terminal (PTY) session in real-time.",
- operationId: "pty.connect",
- responses: {
- 200: {
- description: "Connected session",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- ...errors(404),
- },
- }),
- validator("param", z.object({ ptyID: z.string() })),
- upgradeWebSocket((c) => {
- const id = c.req.param("ptyID")
- let handler: ReturnType<typeof Pty.connect>
- if (!Pty.get(id)) throw new Error("Session not found")
- return {
- onOpen(_event, ws) {
- handler = Pty.connect(id, ws)
- },
- onMessage(event) {
- handler?.onMessage(String(event.data))
- },
- onClose() {
- handler?.onClose()
- },
- }
- }),
- )
-
- .get(
- "/config",
- describeRoute({
- summary: "Get configuration",
- description: "Retrieve the current OpenCode configuration settings and preferences.",
- operationId: "config.get",
- responses: {
- 200: {
- description: "Get config info",
- content: {
- "application/json": {
- schema: resolver(Config.Info),
- },
- },
- },
- },
- }),
- async (c) => {
- return c.json(await Config.get())
- },
- )
-
- .patch(
- "/config",
- describeRoute({
- summary: "Update configuration",
- description: "Update OpenCode configuration settings and preferences.",
- operationId: "config.update",
- responses: {
- 200: {
- description: "Successfully updated config",
- content: {
- "application/json": {
- schema: resolver(Config.Info),
- },
- },
- },
- ...errors(400),
- },
- }),
- validator("json", Config.Info),
- async (c) => {
- const config = c.req.valid("json")
- await Config.update(config)
- return c.json(config)
- },
- )
- .get(
- "/experimental/tool/ids",
- describeRoute({
- summary: "List tool IDs",
- description:
- "Get a list of all available tool IDs, including both built-in tools and dynamically registered tools.",
- operationId: "tool.ids",
- responses: {
- 200: {
- description: "Tool IDs",
- content: {
- "application/json": {
- schema: resolver(z.array(z.string()).meta({ ref: "ToolIDs" })),
- },
- },
- },
- ...errors(400),
- },
- }),
- async (c) => {
- return c.json(await ToolRegistry.ids())
- },
- )
- .get(
- "/experimental/tool",
- describeRoute({
- summary: "List tools",
- description:
- "Get a list of available tools with their JSON schema parameters for a specific provider and model combination.",
- operationId: "tool.list",
- responses: {
- 200: {
- description: "Tools",
- content: {
- "application/json": {
- schema: resolver(
- z
- .array(
- z
- .object({
- id: z.string(),
- description: z.string(),
- parameters: z.any(),
- })
- .meta({ ref: "ToolListItem" }),
- )
- .meta({ ref: "ToolList" }),
- ),
- },
- },
- },
- ...errors(400),
- },
- }),
- validator(
- "query",
- z.object({
- provider: z.string(),
- model: z.string(),
- }),
- ),
- async (c) => {
- const { provider } = c.req.valid("query")
- const tools = await ToolRegistry.tools(provider)
- return c.json(
- tools.map((t) => ({
- id: t.id,
- description: t.description,
- // Handle both Zod schemas and plain JSON schemas
- parameters: (t.parameters as any)?._def ? zodToJsonSchema(t.parameters as any) : t.parameters,
- })),
- )
- },
- )
+ .route("/project", ProjectRoutes())
+ .route("/pty", PtyRoutes())
+ .route("/config", ConfigRoutes())
+ .route("/experimental", ExperimentalRoutes())
+ .route("/session", SessionRoutes())
+ .route("/permission", PermissionRoutes())
+ .route("/question", QuestionRoutes())
+ .route("/provider", ProviderRoutes())
+ .route("/", FileRoutes())
+ .route("/mcp", McpRoutes())
+ .route("/tui", TuiRoutes())
.post(
"/instance/dispose",
describeRoute({
@@ -632,53 +229,6 @@ export namespace Server {
})
},
)
- .post(
- "/experimental/worktree",
- describeRoute({
- summary: "Create worktree",
- description: "Create a new git worktree for the current project.",
- operationId: "worktree.create",
- responses: {
- 200: {
- description: "Worktree created",
- content: {
- "application/json": {
- schema: resolver(Worktree.Info),
- },
- },
- },
- ...errors(400),
- },
- }),
- validator("json", Worktree.create.schema),
- async (c) => {
- const body = c.req.valid("json")
- const worktree = await Worktree.create(body)
- return c.json(worktree)
- },
- )
- .get(
- "/experimental/worktree",
- describeRoute({
- summary: "List worktrees",
- description: "List all sandbox worktrees for the current project.",
- operationId: "worktree.list",
- responses: {
- 200: {
- description: "List of worktree directories",
- content: {
- "application/json": {
- schema: resolver(z.array(z.string())),
- },
- },
- },
- },
- }),
- async (c) => {
- const sandboxes = await Project.sandboxes(Instance.project.id)
- return c.json(sandboxes)
- },
- )
.get(
"/vcs",
describeRoute({
@@ -705,1011 +255,6 @@ export namespace Server {
},
)
.get(
- "/session",
- describeRoute({
- summary: "List sessions",
- description: "Get a list of all OpenCode sessions, sorted by most recently updated.",
- operationId: "session.list",
- responses: {
- 200: {
- description: "List of sessions",
- content: {
- "application/json": {
- schema: resolver(Session.Info.array()),
- },
- },
- },
- },
- }),
- validator(
- "query",
- z.object({
- directory: z.string().optional().meta({ description: "Filter sessions by project directory" }),
- roots: z.coerce.boolean().optional().meta({ description: "Only return root sessions (no parentID)" }),
- start: z.coerce
- .number()
- .optional()
- .meta({ description: "Filter sessions updated on or after this timestamp (milliseconds since epoch)" }),
- search: z.string().optional().meta({ description: "Filter sessions by title (case-insensitive)" }),
- limit: z.coerce.number().optional().meta({ description: "Maximum number of sessions to return" }),
- }),
- ),
- async (c) => {
- const query = c.req.valid("query")
- const term = query.search?.toLowerCase()
- const sessions: Session.Info[] = []
- for await (const session of Session.list()) {
- if (query.directory !== undefined && session.directory !== query.directory) continue
- if (query.roots && session.parentID) continue
- if (query.start !== undefined && session.time.updated < query.start) continue
- if (term !== undefined && !session.title.toLowerCase().includes(term)) continue
- sessions.push(session)
- if (query.limit !== undefined && sessions.length >= query.limit) break
- }
- return c.json(sessions)
- },
- )
- .get(
- "/session/status",
- describeRoute({
- summary: "Get session status",
- description: "Retrieve the current status of all sessions, including active, idle, and completed states.",
- operationId: "session.status",
- responses: {
- 200: {
- description: "Get session status",
- content: {
- "application/json": {
- schema: resolver(z.record(z.string(), SessionStatus.Info)),
- },
- },
- },
- ...errors(400),
- },
- }),
- async (c) => {
- const result = SessionStatus.list()
- return c.json(result)
- },
- )
- .get(
- "/session/:sessionID",
- describeRoute({
- summary: "Get session",
- description: "Retrieve detailed information about a specific OpenCode session.",
- tags: ["Session"],
- operationId: "session.get",
- responses: {
- 200: {
- description: "Get session",
- content: {
- "application/json": {
- schema: resolver(Session.Info),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- sessionID: Session.get.schema,
- }),
- ),
- async (c) => {
- const sessionID = c.req.valid("param").sessionID
- log.info("SEARCH", { url: c.req.url })
- const session = await Session.get(sessionID)
- return c.json(session)
- },
- )
- .get(
- "/session/:sessionID/children",
- describeRoute({
- summary: "Get session children",
- tags: ["Session"],
- description: "Retrieve all child sessions that were forked from the specified parent session.",
- operationId: "session.children",
- responses: {
- 200: {
- description: "List of children",
- content: {
- "application/json": {
- schema: resolver(Session.Info.array()),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- sessionID: Session.children.schema,
- }),
- ),
- async (c) => {
- const sessionID = c.req.valid("param").sessionID
- const session = await Session.children(sessionID)
- return c.json(session)
- },
- )
- .get(
- "/session/:sessionID/todo",
- describeRoute({
- summary: "Get session todos",
- description: "Retrieve the todo list associated with a specific session, showing tasks and action items.",
- operationId: "session.todo",
- responses: {
- 200: {
- description: "Todo list",
- content: {
- "application/json": {
- schema: resolver(Todo.Info.array()),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- sessionID: z.string().meta({ description: "Session ID" }),
- }),
- ),
- async (c) => {
- const sessionID = c.req.valid("param").sessionID
- const todos = await Todo.get(sessionID)
- return c.json(todos)
- },
- )
- .post(
- "/session",
- describeRoute({
- summary: "Create session",
- description: "Create a new OpenCode session for interacting with AI assistants and managing conversations.",
- operationId: "session.create",
- responses: {
- ...errors(400),
- 200: {
- description: "Successfully created session",
- content: {
- "application/json": {
- schema: resolver(Session.Info),
- },
- },
- },
- },
- }),
- validator("json", Session.create.schema.optional()),
- async (c) => {
- const body = c.req.valid("json") ?? {}
- const session = await Session.create(body)
- return c.json(session)
- },
- )
- .delete(
- "/session/:sessionID",
- describeRoute({
- summary: "Delete session",
- description: "Delete a session and permanently remove all associated data, including messages and history.",
- operationId: "session.delete",
- responses: {
- 200: {
- description: "Successfully deleted session",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- sessionID: Session.remove.schema,
- }),
- ),
- async (c) => {
- const sessionID = c.req.valid("param").sessionID
- await Session.remove(sessionID)
- return c.json(true)
- },
- )
- .patch(
- "/session/:sessionID",
- describeRoute({
- summary: "Update session",
- description: "Update properties of an existing session, such as title or other metadata.",
- operationId: "session.update",
- responses: {
- 200: {
- description: "Successfully updated session",
- content: {
- "application/json": {
- schema: resolver(Session.Info),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- sessionID: z.string(),
- }),
- ),
- validator(
- "json",
- z.object({
- title: z.string().optional(),
- time: z
- .object({
- archived: z.number().optional(),
- })
- .optional(),
- }),
- ),
- async (c) => {
- const sessionID = c.req.valid("param").sessionID
- const updates = c.req.valid("json")
-
- const updatedSession = await Session.update(sessionID, (session) => {
- if (updates.title !== undefined) {
- session.title = updates.title
- }
- if (updates.time?.archived !== undefined) session.time.archived = updates.time.archived
- })
-
- return c.json(updatedSession)
- },
- )
- .post(
- "/session/:sessionID/init",
- describeRoute({
- summary: "Initialize session",
- description:
- "Analyze the current application and create an AGENTS.md file with project-specific agent configurations.",
- operationId: "session.init",
- responses: {
- 200: {
- description: "200",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- sessionID: z.string().meta({ description: "Session ID" }),
- }),
- ),
- validator("json", Session.initialize.schema.omit({ sessionID: true })),
- async (c) => {
- const sessionID = c.req.valid("param").sessionID
- const body = c.req.valid("json")
- await Session.initialize({ ...body, sessionID })
- return c.json(true)
- },
- )
- .post(
- "/session/:sessionID/fork",
- describeRoute({
- summary: "Fork session",
- description: "Create a new session by forking an existing session at a specific message point.",
- operationId: "session.fork",
- responses: {
- 200: {
- description: "200",
- content: {
- "application/json": {
- schema: resolver(Session.Info),
- },
- },
- },
- },
- }),
- validator(
- "param",
- z.object({
- sessionID: Session.fork.schema.shape.sessionID,
- }),
- ),
- validator("json", Session.fork.schema.omit({ sessionID: true })),
- async (c) => {
- const sessionID = c.req.valid("param").sessionID
- const body = c.req.valid("json")
- const result = await Session.fork({ ...body, sessionID })
- return c.json(result)
- },
- )
- .post(
- "/session/:sessionID/abort",
- describeRoute({
- summary: "Abort session",
- description: "Abort an active session and stop any ongoing AI processing or command execution.",
- operationId: "session.abort",
- responses: {
- 200: {
- description: "Aborted session",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- sessionID: z.string(),
- }),
- ),
- async (c) => {
- SessionPrompt.cancel(c.req.valid("param").sessionID)
- return c.json(true)
- },
- )
-
- .post(
- "/session/:sessionID/share",
- describeRoute({
- summary: "Share session",
- description: "Create a shareable link for a session, allowing others to view the conversation.",
- operationId: "session.share",
- responses: {
- 200: {
- description: "Successfully shared session",
- content: {
- "application/json": {
- schema: resolver(Session.Info),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- sessionID: z.string(),
- }),
- ),
- async (c) => {
- const sessionID = c.req.valid("param").sessionID
- await Session.share(sessionID)
- const session = await Session.get(sessionID)
- return c.json(session)
- },
- )
- .get(
- "/session/:sessionID/diff",
- describeRoute({
- summary: "Get message diff",
- description: "Get the file changes (diff) that resulted from a specific user message in the session.",
- operationId: "session.diff",
- responses: {
- 200: {
- description: "Successfully retrieved diff",
- content: {
- "application/json": {
- schema: resolver(Snapshot.FileDiff.array()),
- },
- },
- },
- },
- }),
- validator(
- "param",
- z.object({
- sessionID: SessionSummary.diff.schema.shape.sessionID,
- }),
- ),
- validator(
- "query",
- z.object({
- messageID: SessionSummary.diff.schema.shape.messageID,
- }),
- ),
- async (c) => {
- const query = c.req.valid("query")
- const params = c.req.valid("param")
- const result = await SessionSummary.diff({
- sessionID: params.sessionID,
- messageID: query.messageID,
- })
- return c.json(result)
- },
- )
- .delete(
- "/session/:sessionID/share",
- describeRoute({
- summary: "Unshare session",
- description: "Remove the shareable link for a session, making it private again.",
- operationId: "session.unshare",
- responses: {
- 200: {
- description: "Successfully unshared session",
- content: {
- "application/json": {
- schema: resolver(Session.Info),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- sessionID: Session.unshare.schema,
- }),
- ),
- async (c) => {
- const sessionID = c.req.valid("param").sessionID
- await Session.unshare(sessionID)
- const session = await Session.get(sessionID)
- return c.json(session)
- },
- )
- .post(
- "/session/:sessionID/summarize",
- describeRoute({
- summary: "Summarize session",
- description: "Generate a concise summary of the session using AI compaction to preserve key information.",
- operationId: "session.summarize",
- responses: {
- 200: {
- description: "Summarized session",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- sessionID: z.string().meta({ description: "Session ID" }),
- }),
- ),
- validator(
- "json",
- z.object({
- providerID: z.string(),
- modelID: z.string(),
- auto: z.boolean().optional().default(false),
- }),
- ),
- async (c) => {
- const sessionID = c.req.valid("param").sessionID
- const body = c.req.valid("json")
- const session = await Session.get(sessionID)
- await SessionRevert.cleanup(session)
- const msgs = await Session.messages({ sessionID })
- let currentAgent = await Agent.defaultAgent()
- for (let i = msgs.length - 1; i >= 0; i--) {
- const info = msgs[i].info
- if (info.role === "user") {
- currentAgent = info.agent || (await Agent.defaultAgent())
- break
- }
- }
- await SessionCompaction.create({
- sessionID,
- agent: currentAgent,
- model: {
- providerID: body.providerID,
- modelID: body.modelID,
- },
- auto: body.auto,
- })
- await SessionPrompt.loop(sessionID)
- return c.json(true)
- },
- )
- .get(
- "/session/:sessionID/message",
- describeRoute({
- summary: "Get session messages",
- description: "Retrieve all messages in a session, including user prompts and AI responses.",
- operationId: "session.messages",
- responses: {
- 200: {
- description: "List of messages",
- content: {
- "application/json": {
- schema: resolver(MessageV2.WithParts.array()),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- sessionID: z.string().meta({ description: "Session ID" }),
- }),
- ),
- validator(
- "query",
- z.object({
- limit: z.coerce.number().optional(),
- }),
- ),
- async (c) => {
- const query = c.req.valid("query")
- const messages = await Session.messages({
- sessionID: c.req.valid("param").sessionID,
- limit: query.limit,
- })
- return c.json(messages)
- },
- )
- .get(
- "/session/:sessionID/diff",
- describeRoute({
- summary: "Get session diff",
- description: "Get all file changes (diffs) made during this session.",
- operationId: "session.diff",
- responses: {
- 200: {
- description: "List of diffs",
- content: {
- "application/json": {
- schema: resolver(Snapshot.FileDiff.array()),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- sessionID: z.string().meta({ description: "Session ID" }),
- }),
- ),
- async (c) => {
- const diff = await Session.diff(c.req.valid("param").sessionID)
- return c.json(diff)
- },
- )
- .get(
- "/session/:sessionID/message/:messageID",
- describeRoute({
- summary: "Get message",
- description: "Retrieve a specific message from a session by its message ID.",
- operationId: "session.message",
- responses: {
- 200: {
- description: "Message",
- content: {
- "application/json": {
- schema: resolver(
- z.object({
- info: MessageV2.Info,
- parts: MessageV2.Part.array(),
- }),
- ),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- sessionID: z.string().meta({ description: "Session ID" }),
- messageID: z.string().meta({ description: "Message ID" }),
- }),
- ),
- async (c) => {
- const params = c.req.valid("param")
- const message = await MessageV2.get({
- sessionID: params.sessionID,
- messageID: params.messageID,
- })
- return c.json(message)
- },
- )
- .delete(
- "/session/:sessionID/message/:messageID/part/:partID",
- describeRoute({
- description: "Delete a part from a message",
- operationId: "part.delete",
- responses: {
- 200: {
- description: "Successfully deleted part",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- sessionID: z.string().meta({ description: "Session ID" }),
- messageID: z.string().meta({ description: "Message ID" }),
- partID: z.string().meta({ description: "Part ID" }),
- }),
- ),
- async (c) => {
- const params = c.req.valid("param")
- await Session.removePart({
- sessionID: params.sessionID,
- messageID: params.messageID,
- partID: params.partID,
- })
- return c.json(true)
- },
- )
- .patch(
- "/session/:sessionID/message/:messageID/part/:partID",
- describeRoute({
- description: "Update a part in a message",
- operationId: "part.update",
- responses: {
- 200: {
- description: "Successfully updated part",
- content: {
- "application/json": {
- schema: resolver(MessageV2.Part),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- sessionID: z.string().meta({ description: "Session ID" }),
- messageID: z.string().meta({ description: "Message ID" }),
- partID: z.string().meta({ description: "Part ID" }),
- }),
- ),
- validator("json", MessageV2.Part),
- async (c) => {
- const params = c.req.valid("param")
- const body = c.req.valid("json")
- if (
- body.id !== params.partID ||
- body.messageID !== params.messageID ||
- body.sessionID !== params.sessionID
- ) {
- throw new Error(
- `Part mismatch: body.id='${body.id}' vs partID='${params.partID}', body.messageID='${body.messageID}' vs messageID='${params.messageID}', body.sessionID='${body.sessionID}' vs sessionID='${params.sessionID}'`,
- )
- }
- const part = await Session.updatePart(body)
- return c.json(part)
- },
- )
- .post(
- "/session/:sessionID/message",
- describeRoute({
- summary: "Send message",
- description: "Create and send a new message to a session, streaming the AI response.",
- operationId: "session.prompt",
- responses: {
- 200: {
- description: "Created message",
- content: {
- "application/json": {
- schema: resolver(
- z.object({
- info: MessageV2.Assistant,
- parts: MessageV2.Part.array(),
- }),
- ),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- sessionID: z.string().meta({ description: "Session ID" }),
- }),
- ),
- validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
- async (c) => {
- c.status(200)
- c.header("Content-Type", "application/json")
- return stream(c, async (stream) => {
- const sessionID = c.req.valid("param").sessionID
- const body = c.req.valid("json")
- const msg = await SessionPrompt.prompt({ ...body, sessionID })
- stream.write(JSON.stringify(msg))
- })
- },
- )
- .post(
- "/session/:sessionID/prompt_async",
- describeRoute({
- summary: "Send async message",
- description:
- "Create and send a new message to a session asynchronously, starting the session if needed and returning immediately.",
- operationId: "session.prompt_async",
- responses: {
- 204: {
- description: "Prompt accepted",
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- sessionID: z.string().meta({ description: "Session ID" }),
- }),
- ),
- validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
- async (c) => {
- c.status(204)
- c.header("Content-Type", "application/json")
- return stream(c, async () => {
- const sessionID = c.req.valid("param").sessionID
- const body = c.req.valid("json")
- SessionPrompt.prompt({ ...body, sessionID })
- })
- },
- )
- .post(
- "/session/:sessionID/command",
- describeRoute({
- summary: "Send command",
- description: "Send a new command to a session for execution by the AI assistant.",
- operationId: "session.command",
- responses: {
- 200: {
- description: "Created message",
- content: {
- "application/json": {
- schema: resolver(
- z.object({
- info: MessageV2.Assistant,
- parts: MessageV2.Part.array(),
- }),
- ),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- sessionID: z.string().meta({ description: "Session ID" }),
- }),
- ),
- validator("json", SessionPrompt.CommandInput.omit({ sessionID: true })),
- async (c) => {
- const sessionID = c.req.valid("param").sessionID
- const body = c.req.valid("json")
- const msg = await SessionPrompt.command({ ...body, sessionID })
- return c.json(msg)
- },
- )
- .post(
- "/session/:sessionID/shell",
- describeRoute({
- summary: "Run shell command",
- description: "Execute a shell command within the session context and return the AI's response.",
- operationId: "session.shell",
- responses: {
- 200: {
- description: "Created message",
- content: {
- "application/json": {
- schema: resolver(MessageV2.Assistant),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- sessionID: z.string().meta({ description: "Session ID" }),
- }),
- ),
- validator("json", SessionPrompt.ShellInput.omit({ sessionID: true })),
- async (c) => {
- const sessionID = c.req.valid("param").sessionID
- const body = c.req.valid("json")
- const msg = await SessionPrompt.shell({ ...body, sessionID })
- return c.json(msg)
- },
- )
- .post(
- "/session/:sessionID/revert",
- describeRoute({
- summary: "Revert message",
- description:
- "Revert a specific message in a session, undoing its effects and restoring the previous state.",
- operationId: "session.revert",
- responses: {
- 200: {
- description: "Updated session",
- content: {
- "application/json": {
- schema: resolver(Session.Info),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- sessionID: z.string(),
- }),
- ),
- validator("json", SessionRevert.RevertInput.omit({ sessionID: true })),
- async (c) => {
- const sessionID = c.req.valid("param").sessionID
- log.info("revert", c.req.valid("json"))
- const session = await SessionRevert.revert({
- sessionID,
- ...c.req.valid("json"),
- })
- return c.json(session)
- },
- )
- .post(
- "/session/:sessionID/unrevert",
- describeRoute({
- summary: "Restore reverted messages",
- description: "Restore all previously reverted messages in a session.",
- operationId: "session.unrevert",
- responses: {
- 200: {
- description: "Updated session",
- content: {
- "application/json": {
- schema: resolver(Session.Info),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- sessionID: z.string(),
- }),
- ),
- async (c) => {
- const sessionID = c.req.valid("param").sessionID
- const session = await SessionRevert.unrevert({ sessionID })
- return c.json(session)
- },
- )
- .post(
- "/session/:sessionID/permissions/:permissionID",
- describeRoute({
- summary: "Respond to permission",
- deprecated: true,
- description: "Approve or deny a permission request from the AI assistant.",
- operationId: "permission.respond",
- responses: {
- 200: {
- description: "Permission processed successfully",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- sessionID: z.string(),
- permissionID: z.string(),
- }),
- ),
- validator("json", z.object({ response: PermissionNext.Reply })),
- async (c) => {
- const params = c.req.valid("param")
- PermissionNext.reply({
- requestID: params.permissionID,
- reply: c.req.valid("json").response,
- })
- return c.json(true)
- },
- )
- .post(
- "/permission/:requestID/reply",
- describeRoute({
- summary: "Respond to permission request",
- description: "Approve or deny a permission request from the AI assistant.",
- operationId: "permission.reply",
- responses: {
- 200: {
- description: "Permission processed successfully",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- requestID: z.string(),
- }),
- ),
- validator("json", z.object({ reply: PermissionNext.Reply, message: z.string().optional() })),
- async (c) => {
- const params = c.req.valid("param")
- const json = c.req.valid("json")
- await PermissionNext.reply({
- requestID: params.requestID,
- reply: json.reply,
- message: json.message,
- })
- return c.json(true)
- },
- )
- .get(
- "/permission",
- describeRoute({
- summary: "List pending permissions",
- description: "Get all pending permission requests across all sessions.",
- operationId: "permission.list",
- responses: {
- 200: {
- description: "List of pending permissions",
- content: {
- "application/json": {
- schema: resolver(PermissionNext.Request.array()),
- },
- },
- },
- },
- }),
- async (c) => {
- const permissions = await PermissionNext.list()
- return c.json(permissions)
- },
- )
- .route("/question", QuestionRoute)
- .get(
"/command",
describeRoute({
summary: "List commands",
@@ -1731,373 +276,6 @@ export namespace Server {
return c.json(commands)
},
)
- .get(
- "/config/providers",
- describeRoute({
- summary: "List config providers",
- description: "Get a list of all configured AI providers and their default models.",
- operationId: "config.providers",
- responses: {
- 200: {
- description: "List of providers",
- content: {
- "application/json": {
- schema: resolver(
- z.object({
- providers: Provider.Info.array(),
- default: z.record(z.string(), z.string()),
- }),
- ),
- },
- },
- },
- },
- }),
- async (c) => {
- using _ = log.time("providers")
- const providers = await Provider.list().then((x) => mapValues(x, (item) => item))
- return c.json({
- providers: Object.values(providers),
- default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
- })
- },
- )
- .get(
- "/provider",
- describeRoute({
- summary: "List providers",
- description: "Get a list of all available AI providers, including both available and connected ones.",
- operationId: "provider.list",
- responses: {
- 200: {
- description: "List of providers",
- content: {
- "application/json": {
- schema: resolver(
- z.object({
- all: ModelsDev.Provider.array(),
- default: z.record(z.string(), z.string()),
- connected: z.array(z.string()),
- }),
- ),
- },
- },
- },
- },
- }),
- async (c) => {
- const config = await Config.get()
- const disabled = new Set(config.disabled_providers ?? [])
- const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
-
- const allProviders = await ModelsDev.get()
- const filteredProviders: Record<string, (typeof allProviders)[string]> = {}
- for (const [key, value] of Object.entries(allProviders)) {
- if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) {
- filteredProviders[key] = value
- }
- }
-
- const connected = await Provider.list()
- const providers = Object.assign(
- mapValues(filteredProviders, (x) => Provider.fromModelsDevProvider(x)),
- connected,
- )
- return c.json({
- all: Object.values(providers),
- default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
- connected: Object.keys(connected),
- })
- },
- )
- .get(
- "/provider/auth",
- describeRoute({
- summary: "Get provider auth methods",
- description: "Retrieve available authentication methods for all AI providers.",
- operationId: "provider.auth",
- responses: {
- 200: {
- description: "Provider auth methods",
- content: {
- "application/json": {
- schema: resolver(z.record(z.string(), z.array(ProviderAuth.Method))),
- },
- },
- },
- },
- }),
- async (c) => {
- return c.json(await ProviderAuth.methods())
- },
- )
- .post(
- "/provider/:providerID/oauth/authorize",
- describeRoute({
- summary: "OAuth authorize",
- description: "Initiate OAuth authorization for a specific AI provider to get an authorization URL.",
- operationId: "provider.oauth.authorize",
- responses: {
- 200: {
- description: "Authorization URL and method",
- content: {
- "application/json": {
- schema: resolver(ProviderAuth.Authorization.optional()),
- },
- },
- },
- ...errors(400),
- },
- }),
- validator(
- "param",
- z.object({
- providerID: z.string().meta({ description: "Provider ID" }),
- }),
- ),
- validator(
- "json",
- z.object({
- method: z.number().meta({ description: "Auth method index" }),
- }),
- ),
- async (c) => {
- const providerID = c.req.valid("param").providerID
- const { method } = c.req.valid("json")
- const result = await ProviderAuth.authorize({
- providerID,
- method,
- })
- return c.json(result)
- },
- )
- .post(
- "/provider/:providerID/oauth/callback",
- describeRoute({
- summary: "OAuth callback",
- description: "Handle the OAuth callback from a provider after user authorization.",
- operationId: "provider.oauth.callback",
- responses: {
- 200: {
- description: "OAuth callback processed successfully",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- ...errors(400),
- },
- }),
- validator(
- "param",
- z.object({
- providerID: z.string().meta({ description: "Provider ID" }),
- }),
- ),
- validator(
- "json",
- z.object({
- method: z.number().meta({ description: "Auth method index" }),
- code: z.string().optional().meta({ description: "OAuth authorization code" }),
- }),
- ),
- async (c) => {
- const providerID = c.req.valid("param").providerID
- const { method, code } = c.req.valid("json")
- await ProviderAuth.callback({
- providerID,
- method,
- code,
- })
- return c.json(true)
- },
- )
- .get(
- "/find",
- describeRoute({
- summary: "Find text",
- description: "Search for text patterns across files in the project using ripgrep.",
- operationId: "find.text",
- responses: {
- 200: {
- description: "Matches",
- content: {
- "application/json": {
- schema: resolver(Ripgrep.Match.shape.data.array()),
- },
- },
- },
- },
- }),
- validator(
- "query",
- z.object({
- pattern: z.string(),
- }),
- ),
- async (c) => {
- const pattern = c.req.valid("query").pattern
- const result = await Ripgrep.search({
- cwd: Instance.directory,
- pattern,
- limit: 10,
- })
- return c.json(result)
- },
- )
- .get(
- "/find/file",
- describeRoute({
- summary: "Find files",
- description: "Search for files or directories by name or pattern in the project directory.",
- operationId: "find.files",
- responses: {
- 200: {
- description: "File paths",
- content: {
- "application/json": {
- schema: resolver(z.string().array()),
- },
- },
- },
- },
- }),
- validator(
- "query",
- z.object({
- query: z.string(),
- dirs: z.enum(["true", "false"]).optional(),
- type: z.enum(["file", "directory"]).optional(),
- limit: z.coerce.number().int().min(1).max(200).optional(),
- }),
- ),
- async (c) => {
- const query = c.req.valid("query").query
- const dirs = c.req.valid("query").dirs
- const type = c.req.valid("query").type
- const limit = c.req.valid("query").limit
- const results = await File.search({
- query,
- limit: limit ?? 10,
- dirs: dirs !== "false",
- type,
- })
- return c.json(results)
- },
- )
- .get(
- "/find/symbol",
- describeRoute({
- summary: "Find symbols",
- description: "Search for workspace symbols like functions, classes, and variables using LSP.",
- operationId: "find.symbols",
- responses: {
- 200: {
- description: "Symbols",
- content: {
- "application/json": {
- schema: resolver(LSP.Symbol.array()),
- },
- },
- },
- },
- }),
- validator(
- "query",
- z.object({
- query: z.string(),
- }),
- ),
- async (c) => {
- /*
- const query = c.req.valid("query").query
- const result = await LSP.workspaceSymbol(query)
- return c.json(result)
- */
- return c.json([])
- },
- )
- .get(
- "/file",
- describeRoute({
- summary: "List files",
- description: "List files and directories in a specified path.",
- operationId: "file.list",
- responses: {
- 200: {
- description: "Files and directories",
- content: {
- "application/json": {
- schema: resolver(File.Node.array()),
- },
- },
- },
- },
- }),
- validator(
- "query",
- z.object({
- path: z.string(),
- }),
- ),
- async (c) => {
- const path = c.req.valid("query").path
- const content = await File.list(path)
- return c.json(content)
- },
- )
- .get(
- "/file/content",
- describeRoute({
- summary: "Read file",
- description: "Read the content of a specified file.",
- operationId: "file.read",
- responses: {
- 200: {
- description: "File content",
- content: {
- "application/json": {
- schema: resolver(File.Content),
- },
- },
- },
- },
- }),
- validator(
- "query",
- z.object({
- path: z.string(),
- }),
- ),
- async (c) => {
- const path = c.req.valid("query").path
- const content = await File.read(path)
- return c.json(content)
- },
- )
- .get(
- "/file/status",
- describeRoute({
- summary: "Get file status",
- description: "Get the git status of all files in the project.",
- operationId: "file.status",
- responses: {
- 200: {
- description: "File status",
- content: {
- "application/json": {
- schema: resolver(File.Info.array()),
- },
- },
- },
- },
- }),
- async (c) => {
- const content = await File.status()
- return c.json(content)
- },
- )
.post(
"/log",
describeRoute({
@@ -2173,238 +351,25 @@ export namespace Server {
},
)
.get(
- "/mcp",
- describeRoute({
- summary: "Get MCP status",
- description: "Get the status of all Model Context Protocol (MCP) servers.",
- operationId: "mcp.status",
- responses: {
- 200: {
- description: "MCP server status",
- content: {
- "application/json": {
- schema: resolver(z.record(z.string(), MCP.Status)),
- },
- },
- },
- },
- }),
- async (c) => {
- return c.json(await MCP.status())
- },
- )
- .post(
- "/mcp",
- describeRoute({
- summary: "Add MCP server",
- description: "Dynamically add a new Model Context Protocol (MCP) server to the system.",
- operationId: "mcp.add",
- responses: {
- 200: {
- description: "MCP server added successfully",
- content: {
- "application/json": {
- schema: resolver(z.record(z.string(), MCP.Status)),
- },
- },
- },
- ...errors(400),
- },
- }),
- validator(
- "json",
- z.object({
- name: z.string(),
- config: Config.Mcp,
- }),
- ),
- async (c) => {
- const { name, config } = c.req.valid("json")
- const result = await MCP.add(name, config)
- return c.json(result.status)
- },
- )
- .post(
- "/mcp/:name/auth",
- describeRoute({
- summary: "Start MCP OAuth",
- description: "Start OAuth authentication flow for a Model Context Protocol (MCP) server.",
- operationId: "mcp.auth.start",
- responses: {
- 200: {
- description: "OAuth flow started",
- content: {
- "application/json": {
- schema: resolver(
- z.object({
- authorizationUrl: z.string().describe("URL to open in browser for authorization"),
- }),
- ),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- async (c) => {
- const name = c.req.param("name")
- const supportsOAuth = await MCP.supportsOAuth(name)
- if (!supportsOAuth) {
- return c.json({ error: `MCP server ${name} does not support OAuth` }, 400)
- }
- const result = await MCP.startAuth(name)
- return c.json(result)
- },
- )
- .post(
- "/mcp/:name/auth/callback",
+ "/skill",
describeRoute({
- summary: "Complete MCP OAuth",
- description:
- "Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code.",
- operationId: "mcp.auth.callback",
+ summary: "List skills",
+ description: "Get a list of all available skills in the OpenCode system.",
+ operationId: "app.skills",
responses: {
200: {
- description: "OAuth authentication completed",
+ description: "List of skills",
content: {
"application/json": {
- schema: resolver(MCP.Status),
+ schema: resolver(Skill.Info.array()),
},
},
},
- ...errors(400, 404),
},
}),
- validator(
- "json",
- z.object({
- code: z.string().describe("Authorization code from OAuth callback"),
- }),
- ),
async (c) => {
- const name = c.req.param("name")
- const { code } = c.req.valid("json")
- const status = await MCP.finishAuth(name, code)
- return c.json(status)
- },
- )
- .post(
- "/mcp/:name/auth/authenticate",
- describeRoute({
- summary: "Authenticate MCP OAuth",
- description: "Start OAuth flow and wait for callback (opens browser)",
- operationId: "mcp.auth.authenticate",
- responses: {
- 200: {
- description: "OAuth authentication completed",
- content: {
- "application/json": {
- schema: resolver(MCP.Status),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- async (c) => {
- const name = c.req.param("name")
- const supportsOAuth = await MCP.supportsOAuth(name)
- if (!supportsOAuth) {
- return c.json({ error: `MCP server ${name} does not support OAuth` }, 400)
- }
- const status = await MCP.authenticate(name)
- return c.json(status)
- },
- )
- .delete(
- "/mcp/:name/auth",
- describeRoute({
- summary: "Remove MCP OAuth",
- description: "Remove OAuth credentials for an MCP server",
- operationId: "mcp.auth.remove",
- responses: {
- 200: {
- description: "OAuth credentials removed",
- content: {
- "application/json": {
- schema: resolver(z.object({ success: z.literal(true) })),
- },
- },
- },
- ...errors(404),
- },
- }),
- async (c) => {
- const name = c.req.param("name")
- await MCP.removeAuth(name)
- return c.json({ success: true as const })
- },
- )
- .post(
- "/mcp/:name/connect",
- describeRoute({
- description: "Connect an MCP server",
- operationId: "mcp.connect",
- responses: {
- 200: {
- description: "MCP server connected successfully",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- },
- }),
- validator("param", z.object({ name: z.string() })),
- async (c) => {
- const { name } = c.req.valid("param")
- await MCP.connect(name)
- return c.json(true)
- },
- )
- .post(
- "/mcp/:name/disconnect",
- describeRoute({
- description: "Disconnect an MCP server",
- operationId: "mcp.disconnect",
- responses: {
- 200: {
- description: "MCP server disconnected successfully",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- },
- }),
- validator("param", z.object({ name: z.string() })),
- async (c) => {
- const { name } = c.req.valid("param")
- await MCP.disconnect(name)
- return c.json(true)
- },
- )
- .get(
- "/experimental/resource",
- describeRoute({
- summary: "Get MCP resources",
- description: "Get all available MCP resources from connected servers. Optionally filter by name.",
- operationId: "experimental.resource.list",
- responses: {
- 200: {
- description: "MCP resources",
- content: {
- "application/json": {
- schema: resolver(z.record(z.string(), MCP.Resource)),
- },
- },
- },
- },
- }),
- async (c) => {
- return c.json(await MCP.resources())
+ const skills = await Skill.all()
+ return c.json(skills)
},
)
.get(
@@ -2449,301 +414,6 @@ export namespace Server {
return c.json(await Format.status())
},
)
- .post(
- "/tui/append-prompt",
- describeRoute({
- summary: "Append TUI prompt",
- description: "Append prompt to the TUI",
- operationId: "tui.appendPrompt",
- responses: {
- 200: {
- description: "Prompt processed successfully",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- ...errors(400),
- },
- }),
- validator("json", TuiEvent.PromptAppend.properties),
- async (c) => {
- await Bus.publish(TuiEvent.PromptAppend, c.req.valid("json"))
- return c.json(true)
- },
- )
- .post(
- "/tui/open-help",
- describeRoute({
- summary: "Open help dialog",
- description: "Open the help dialog in the TUI to display user assistance information.",
- operationId: "tui.openHelp",
- responses: {
- 200: {
- description: "Help dialog opened successfully",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- },
- }),
- async (c) => {
- // TODO: open dialog
- return c.json(true)
- },
- )
- .post(
- "/tui/open-sessions",
- describeRoute({
- summary: "Open sessions dialog",
- description: "Open the session dialog",
- operationId: "tui.openSessions",
- responses: {
- 200: {
- description: "Session dialog opened successfully",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- },
- }),
- async (c) => {
- await Bus.publish(TuiEvent.CommandExecute, {
- command: "session.list",
- })
- return c.json(true)
- },
- )
- .post(
- "/tui/open-themes",
- describeRoute({
- summary: "Open themes dialog",
- description: "Open the theme dialog",
- operationId: "tui.openThemes",
- responses: {
- 200: {
- description: "Theme dialog opened successfully",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- },
- }),
- async (c) => {
- await Bus.publish(TuiEvent.CommandExecute, {
- command: "session.list",
- })
- return c.json(true)
- },
- )
- .post(
- "/tui/open-models",
- describeRoute({
- summary: "Open models dialog",
- description: "Open the model dialog",
- operationId: "tui.openModels",
- responses: {
- 200: {
- description: "Model dialog opened successfully",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- },
- }),
- async (c) => {
- await Bus.publish(TuiEvent.CommandExecute, {
- command: "model.list",
- })
- return c.json(true)
- },
- )
- .post(
- "/tui/submit-prompt",
- describeRoute({
- summary: "Submit TUI prompt",
- description: "Submit the prompt",
- operationId: "tui.submitPrompt",
- responses: {
- 200: {
- description: "Prompt submitted successfully",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- },
- }),
- async (c) => {
- await Bus.publish(TuiEvent.CommandExecute, {
- command: "prompt.submit",
- })
- return c.json(true)
- },
- )
- .post(
- "/tui/clear-prompt",
- describeRoute({
- summary: "Clear TUI prompt",
- description: "Clear the prompt",
- operationId: "tui.clearPrompt",
- responses: {
- 200: {
- description: "Prompt cleared successfully",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- },
- }),
- async (c) => {
- await Bus.publish(TuiEvent.CommandExecute, {
- command: "prompt.clear",
- })
- return c.json(true)
- },
- )
- .post(
- "/tui/execute-command",
- describeRoute({
- summary: "Execute TUI command",
- description: "Execute a TUI command (e.g. agent_cycle)",
- operationId: "tui.executeCommand",
- responses: {
- 200: {
- description: "Command executed successfully",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- ...errors(400),
- },
- }),
- validator("json", z.object({ command: z.string() })),
- async (c) => {
- const command = c.req.valid("json").command
- await Bus.publish(TuiEvent.CommandExecute, {
- // @ts-expect-error
- command: {
- session_new: "session.new",
- session_share: "session.share",
- session_interrupt: "session.interrupt",
- session_compact: "session.compact",
- messages_page_up: "session.page.up",
- messages_page_down: "session.page.down",
- messages_half_page_up: "session.half.page.up",
- messages_half_page_down: "session.half.page.down",
- messages_first: "session.first",
- messages_last: "session.last",
- agent_cycle: "agent.cycle",
- }[command],
- })
- return c.json(true)
- },
- )
- .post(
- "/tui/show-toast",
- describeRoute({
- summary: "Show TUI toast",
- description: "Show a toast notification in the TUI",
- operationId: "tui.showToast",
- responses: {
- 200: {
- description: "Toast notification shown successfully",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- },
- }),
- validator("json", TuiEvent.ToastShow.properties),
- async (c) => {
- await Bus.publish(TuiEvent.ToastShow, c.req.valid("json"))
- return c.json(true)
- },
- )
- .post(
- "/tui/publish",
- describeRoute({
- summary: "Publish TUI event",
- description: "Publish a TUI event",
- operationId: "tui.publish",
- responses: {
- 200: {
- description: "Event published successfully",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- ...errors(400),
- },
- }),
- validator(
- "json",
- z.union(
- Object.values(TuiEvent).map((def) => {
- return z
- .object({
- type: z.literal(def.type),
- properties: def.properties,
- })
- .meta({
- ref: "Event" + "." + def.type,
- })
- }),
- ),
- ),
- async (c) => {
- const evt = c.req.valid("json")
- await Bus.publish(Object.values(TuiEvent).find((def) => def.type === evt.type)!, evt.properties)
- return c.json(true)
- },
- )
- .post(
- "/tui/select-session",
- describeRoute({
- summary: "Select session",
- description: "Navigate the TUI to display the specified session.",
- operationId: "tui.selectSession",
- responses: {
- 200: {
- description: "Session selected successfully",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator("json", TuiEvent.SessionSelect.properties),
- async (c) => {
- const { sessionID } = c.req.valid("json")
- await Session.get(sessionID)
- await Bus.publish(TuiEvent.SessionSelect, { sessionID })
- return c.json(true)
- },
- )
- .route("/tui/control", TuiRoute)
.put(
"/auth/:providerID",
describeRoute({
diff --git a/packages/opencode/src/server/tui.ts b/packages/opencode/src/server/tui.ts
deleted file mode 100644
index 42821ad9e..000000000
--- a/packages/opencode/src/server/tui.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import { Hono, type Context } from "hono"
-import { describeRoute, resolver, validator } from "hono-openapi"
-import { z } from "zod"
-import { AsyncQueue } from "../util/queue"
-
-const TuiRequest = z.object({
- path: z.string(),
- body: z.any(),
-})
-
-type TuiRequest = z.infer<typeof TuiRequest>
-
-const request = new AsyncQueue<TuiRequest>()
-const response = new AsyncQueue<any>()
-
-export async function callTui(ctx: Context) {
- const body = await ctx.req.json()
- request.push({
- path: ctx.req.path,
- body,
- })
- return response.next()
-}
-
-export const TuiRoute = new Hono()
- .get(
- "/next",
- describeRoute({
- summary: "Get next TUI request",
- description: "Retrieve the next TUI (Terminal User Interface) request from the queue for processing.",
- operationId: "tui.control.next",
- responses: {
- 200: {
- description: "Next TUI request",
- content: {
- "application/json": {
- schema: resolver(TuiRequest),
- },
- },
- },
- },
- }),
- async (c) => {
- const req = await request.next()
- return c.json(req)
- },
- )
- .post(
- "/response",
- describeRoute({
- summary: "Submit TUI response",
- description: "Submit a response to the TUI request queue to complete a pending request.",
- operationId: "tui.control.response",
- responses: {
- 200: {
- description: "Response submitted successfully",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- },
- }),
- validator("json", z.any()),
- async (c) => {
- const body = c.req.valid("json")
- response.push(body)
- return c.json(true)
- },
- )