import { Hono, type Context } from "hono" import { describeRoute, validator, resolver } from "hono-openapi" import { Schema } from "effect" import z from "zod" import { Bus } from "@/bus" import { Session } from "@/session/session" import type { SessionID } from "@/session/schema" import { TuiEvent } from "@/cli/cmd/tui/event" import { zodObject } from "@/util/effect-zod" import { AsyncQueue } from "@/util/queue" import { errors } from "../../error" import { lazy } from "@/util/lazy" import { runRequest } from "./trace" export const TuiRequest = z.object({ path: z.string(), body: z.any(), }) export type TuiRequest = z.infer const request = new AsyncQueue() const response = new AsyncQueue() export function nextTuiRequest() { return request.next() } export function submitTuiResponse(body: unknown) { response.push(body) } 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 nextTuiRequest() 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") submitTuiResponse(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", zodObject(TuiEvent.PromptAppend.properties)), async (c) => { await Bus.publish(TuiEvent.PromptAppend, c.req.valid("json") as { text: string }) 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) => { await Bus.publish(TuiEvent.CommandExecute, { command: "help.show", }) 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_line_up: "session.line.up", messages_line_down: "session.line.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", zodObject(TuiEvent.ToastShow.properties)), async (c) => { await Bus.publish( TuiEvent.ToastShow, c.req.valid("json") as Schema.Schema.Type, ) 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: zodObject(def.properties), }) .meta({ ref: `Event.${def.type}`, }) }), ), ), async (c) => { const evt = c.req.valid("json") as { type: string; properties: Record } // eslint-disable-next-line @typescript-eslint/no-explicit-any await Bus.publish(Object.values(TuiEvent).find((def) => def.type === evt.type)! as any, evt.properties as any) 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", zodObject(TuiEvent.SessionSelect.properties)), async (c) => { const { sessionID } = c.req.valid("json") as { sessionID: SessionID } await runRequest( "TuiRoutes.sessionSelect", c, Session.Service.use((svc) => svc.get(sessionID)), ) await Bus.publish(TuiEvent.SessionSelect, { sessionID }) return c.json(true) }, ) .route("/control", TuiControlRoutes), )