tui.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. import { Hono, type Context } from "hono"
  2. import { describeRoute, validator, resolver } from "hono-openapi"
  3. import z from "zod"
  4. import { Bus } from "../../bus"
  5. import { Session } from "../../session"
  6. import { TuiEvent } from "@/cli/cmd/tui/event"
  7. import { AppRuntime } from "@/effect/app-runtime"
  8. import { AsyncQueue } from "../../util/queue"
  9. import { errors } from "../error"
  10. import { lazy } from "../../util/lazy"
  11. const TuiRequest = z.object({
  12. path: z.string(),
  13. body: z.any(),
  14. })
  15. type TuiRequest = z.infer<typeof TuiRequest>
  16. const request = new AsyncQueue<TuiRequest>()
  17. const response = new AsyncQueue<any>()
  18. export async function callTui(ctx: Context) {
  19. const body = await ctx.req.json()
  20. request.push({
  21. path: ctx.req.path,
  22. body,
  23. })
  24. return response.next()
  25. }
  26. const TuiControlRoutes = new Hono()
  27. .get(
  28. "/next",
  29. describeRoute({
  30. summary: "Get next TUI request",
  31. description: "Retrieve the next TUI (Terminal User Interface) request from the queue for processing.",
  32. operationId: "tui.control.next",
  33. responses: {
  34. 200: {
  35. description: "Next TUI request",
  36. content: {
  37. "application/json": {
  38. schema: resolver(TuiRequest),
  39. },
  40. },
  41. },
  42. },
  43. }),
  44. async (c) => {
  45. const req = await request.next()
  46. return c.json(req)
  47. },
  48. )
  49. .post(
  50. "/response",
  51. describeRoute({
  52. summary: "Submit TUI response",
  53. description: "Submit a response to the TUI request queue to complete a pending request.",
  54. operationId: "tui.control.response",
  55. responses: {
  56. 200: {
  57. description: "Response submitted successfully",
  58. content: {
  59. "application/json": {
  60. schema: resolver(z.boolean()),
  61. },
  62. },
  63. },
  64. },
  65. }),
  66. validator("json", z.any()),
  67. async (c) => {
  68. const body = c.req.valid("json")
  69. response.push(body)
  70. return c.json(true)
  71. },
  72. )
  73. export const TuiRoutes = lazy(() =>
  74. new Hono()
  75. .post(
  76. "/append-prompt",
  77. describeRoute({
  78. summary: "Append TUI prompt",
  79. description: "Append prompt to the TUI",
  80. operationId: "tui.appendPrompt",
  81. responses: {
  82. 200: {
  83. description: "Prompt processed successfully",
  84. content: {
  85. "application/json": {
  86. schema: resolver(z.boolean()),
  87. },
  88. },
  89. },
  90. ...errors(400),
  91. },
  92. }),
  93. validator("json", TuiEvent.PromptAppend.properties),
  94. async (c) => {
  95. await Bus.publish(TuiEvent.PromptAppend, c.req.valid("json"))
  96. return c.json(true)
  97. },
  98. )
  99. .post(
  100. "/open-help",
  101. describeRoute({
  102. summary: "Open help dialog",
  103. description: "Open the help dialog in the TUI to display user assistance information.",
  104. operationId: "tui.openHelp",
  105. responses: {
  106. 200: {
  107. description: "Help dialog opened successfully",
  108. content: {
  109. "application/json": {
  110. schema: resolver(z.boolean()),
  111. },
  112. },
  113. },
  114. },
  115. }),
  116. async (c) => {
  117. await Bus.publish(TuiEvent.CommandExecute, {
  118. command: "help.show",
  119. })
  120. return c.json(true)
  121. },
  122. )
  123. .post(
  124. "/open-sessions",
  125. describeRoute({
  126. summary: "Open sessions dialog",
  127. description: "Open the session dialog",
  128. operationId: "tui.openSessions",
  129. responses: {
  130. 200: {
  131. description: "Session dialog opened successfully",
  132. content: {
  133. "application/json": {
  134. schema: resolver(z.boolean()),
  135. },
  136. },
  137. },
  138. },
  139. }),
  140. async (c) => {
  141. await Bus.publish(TuiEvent.CommandExecute, {
  142. command: "session.list",
  143. })
  144. return c.json(true)
  145. },
  146. )
  147. .post(
  148. "/open-themes",
  149. describeRoute({
  150. summary: "Open themes dialog",
  151. description: "Open the theme dialog",
  152. operationId: "tui.openThemes",
  153. responses: {
  154. 200: {
  155. description: "Theme dialog opened successfully",
  156. content: {
  157. "application/json": {
  158. schema: resolver(z.boolean()),
  159. },
  160. },
  161. },
  162. },
  163. }),
  164. async (c) => {
  165. await Bus.publish(TuiEvent.CommandExecute, {
  166. command: "session.list",
  167. })
  168. return c.json(true)
  169. },
  170. )
  171. .post(
  172. "/open-models",
  173. describeRoute({
  174. summary: "Open models dialog",
  175. description: "Open the model dialog",
  176. operationId: "tui.openModels",
  177. responses: {
  178. 200: {
  179. description: "Model dialog opened successfully",
  180. content: {
  181. "application/json": {
  182. schema: resolver(z.boolean()),
  183. },
  184. },
  185. },
  186. },
  187. }),
  188. async (c) => {
  189. await Bus.publish(TuiEvent.CommandExecute, {
  190. command: "model.list",
  191. })
  192. return c.json(true)
  193. },
  194. )
  195. .post(
  196. "/submit-prompt",
  197. describeRoute({
  198. summary: "Submit TUI prompt",
  199. description: "Submit the prompt",
  200. operationId: "tui.submitPrompt",
  201. responses: {
  202. 200: {
  203. description: "Prompt submitted successfully",
  204. content: {
  205. "application/json": {
  206. schema: resolver(z.boolean()),
  207. },
  208. },
  209. },
  210. },
  211. }),
  212. async (c) => {
  213. await Bus.publish(TuiEvent.CommandExecute, {
  214. command: "prompt.submit",
  215. })
  216. return c.json(true)
  217. },
  218. )
  219. .post(
  220. "/clear-prompt",
  221. describeRoute({
  222. summary: "Clear TUI prompt",
  223. description: "Clear the prompt",
  224. operationId: "tui.clearPrompt",
  225. responses: {
  226. 200: {
  227. description: "Prompt cleared successfully",
  228. content: {
  229. "application/json": {
  230. schema: resolver(z.boolean()),
  231. },
  232. },
  233. },
  234. },
  235. }),
  236. async (c) => {
  237. await Bus.publish(TuiEvent.CommandExecute, {
  238. command: "prompt.clear",
  239. })
  240. return c.json(true)
  241. },
  242. )
  243. .post(
  244. "/execute-command",
  245. describeRoute({
  246. summary: "Execute TUI command",
  247. description: "Execute a TUI command (e.g. agent_cycle)",
  248. operationId: "tui.executeCommand",
  249. responses: {
  250. 200: {
  251. description: "Command executed successfully",
  252. content: {
  253. "application/json": {
  254. schema: resolver(z.boolean()),
  255. },
  256. },
  257. },
  258. ...errors(400),
  259. },
  260. }),
  261. validator("json", z.object({ command: z.string() })),
  262. async (c) => {
  263. const command = c.req.valid("json").command
  264. await Bus.publish(TuiEvent.CommandExecute, {
  265. // @ts-expect-error
  266. command: {
  267. session_new: "session.new",
  268. session_share: "session.share",
  269. session_interrupt: "session.interrupt",
  270. session_compact: "session.compact",
  271. messages_page_up: "session.page.up",
  272. messages_page_down: "session.page.down",
  273. messages_line_up: "session.line.up",
  274. messages_line_down: "session.line.down",
  275. messages_half_page_up: "session.half.page.up",
  276. messages_half_page_down: "session.half.page.down",
  277. messages_first: "session.first",
  278. messages_last: "session.last",
  279. agent_cycle: "agent.cycle",
  280. }[command],
  281. })
  282. return c.json(true)
  283. },
  284. )
  285. .post(
  286. "/show-toast",
  287. describeRoute({
  288. summary: "Show TUI toast",
  289. description: "Show a toast notification in the TUI",
  290. operationId: "tui.showToast",
  291. responses: {
  292. 200: {
  293. description: "Toast notification shown successfully",
  294. content: {
  295. "application/json": {
  296. schema: resolver(z.boolean()),
  297. },
  298. },
  299. },
  300. },
  301. }),
  302. validator("json", TuiEvent.ToastShow.properties),
  303. async (c) => {
  304. await Bus.publish(TuiEvent.ToastShow, c.req.valid("json"))
  305. return c.json(true)
  306. },
  307. )
  308. .post(
  309. "/publish",
  310. describeRoute({
  311. summary: "Publish TUI event",
  312. description: "Publish a TUI event",
  313. operationId: "tui.publish",
  314. responses: {
  315. 200: {
  316. description: "Event published successfully",
  317. content: {
  318. "application/json": {
  319. schema: resolver(z.boolean()),
  320. },
  321. },
  322. },
  323. ...errors(400),
  324. },
  325. }),
  326. validator(
  327. "json",
  328. z.union(
  329. Object.values(TuiEvent).map((def) => {
  330. return z
  331. .object({
  332. type: z.literal(def.type),
  333. properties: def.properties,
  334. })
  335. .meta({
  336. ref: "Event" + "." + def.type,
  337. })
  338. }),
  339. ),
  340. ),
  341. async (c) => {
  342. const evt = c.req.valid("json")
  343. await Bus.publish(Object.values(TuiEvent).find((def) => def.type === evt.type)!, evt.properties)
  344. return c.json(true)
  345. },
  346. )
  347. .post(
  348. "/select-session",
  349. describeRoute({
  350. summary: "Select session",
  351. description: "Navigate the TUI to display the specified session.",
  352. operationId: "tui.selectSession",
  353. responses: {
  354. 200: {
  355. description: "Session selected successfully",
  356. content: {
  357. "application/json": {
  358. schema: resolver(z.boolean()),
  359. },
  360. },
  361. },
  362. ...errors(400, 404),
  363. },
  364. }),
  365. validator("json", TuiEvent.SessionSelect.properties),
  366. async (c) => {
  367. const { sessionID } = c.req.valid("json")
  368. await AppRuntime.runPromise(Session.Service.use((svc) => svc.get(sessionID)))
  369. await Bus.publish(TuiEvent.SessionSelect, { sessionID })
  370. return c.json(true)
  371. },
  372. )
  373. .route("/control", TuiControlRoutes),
  374. )