adapters.tsx 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. import type { TuiDialogSelectOption, TuiPluginApi, TuiSlotProps } from "@kirincode-ai/plugin/tui"
  2. import type { TuiConfig } from "../config"
  3. import type { useEvent } from "../context/event"
  4. import type { useRoute } from "../context/route"
  5. import type { useSDK } from "../context/sdk"
  6. import type { useSync } from "../context/sync"
  7. import type { useTheme } from "../context/theme"
  8. import { Dialog as DialogUI, type useDialog } from "../ui/dialog"
  9. import type { useOpencodeKeymap } from "../keymap"
  10. import type { useKV } from "../context/kv"
  11. import { DialogAlert } from "../ui/dialog-alert"
  12. import { DialogConfirm } from "../ui/dialog-confirm"
  13. import { DialogPrompt } from "../ui/dialog-prompt"
  14. import { DialogSelect, type DialogSelectOption as SelectOption } from "../ui/dialog-select"
  15. import { Prompt } from "../component/prompt"
  16. import type { useToast } from "../ui/toast"
  17. import * as Keymap from "../keymap"
  18. import { createCommandShim } from "./command-shim"
  19. import type { PluginRoutes } from "./api"
  20. export type { RouteMap } from "./api"
  21. export { createPluginRoutes, createTuiApi } from "./api"
  22. type Input = {
  23. version: string
  24. tuiConfig: TuiConfig.Resolved
  25. dialog: ReturnType<typeof useDialog>
  26. keymap: ReturnType<typeof useOpencodeKeymap>
  27. kv: ReturnType<typeof useKV>
  28. route: ReturnType<typeof useRoute>
  29. routes: PluginRoutes
  30. event: ReturnType<typeof useEvent>
  31. sdk: ReturnType<typeof useSDK>
  32. sync: ReturnType<typeof useSync>
  33. theme: ReturnType<typeof useTheme>
  34. toast: ReturnType<typeof useToast>
  35. renderer: TuiPluginApi["renderer"]
  36. attention: TuiPluginApi["attention"]
  37. Slot: TuiPluginApi["ui"]["Slot"]
  38. }
  39. function routeNavigate(route: ReturnType<typeof useRoute>, name: string, params?: Record<string, unknown>) {
  40. if (name === "home") {
  41. route.navigate({ type: "home" })
  42. return
  43. }
  44. if (name === "session") {
  45. const sessionID = params?.sessionID
  46. if (typeof sessionID !== "string") return
  47. route.navigate({ type: "session", sessionID })
  48. return
  49. }
  50. route.navigate({ type: "plugin", id: name, data: params })
  51. }
  52. function routeCurrent(route: ReturnType<typeof useRoute>): TuiPluginApi["route"]["current"] {
  53. if (route.data.type === "home") return { name: "home" }
  54. if (route.data.type === "session") {
  55. return {
  56. name: "session",
  57. params: {
  58. sessionID: route.data.sessionID,
  59. prompt: route.data.prompt,
  60. },
  61. }
  62. }
  63. return {
  64. name: route.data.id,
  65. params: route.data.data,
  66. }
  67. }
  68. function mapOption<Value>(item: TuiDialogSelectOption<Value>): SelectOption<Value> {
  69. return {
  70. ...item,
  71. onSelect: () => item.onSelect?.(),
  72. }
  73. }
  74. function pickOption<Value>(item: SelectOption<Value>): TuiDialogSelectOption<Value> {
  75. return {
  76. title: item.title,
  77. value: item.value,
  78. description: item.description,
  79. footer: item.footer,
  80. category: item.category,
  81. disabled: item.disabled,
  82. }
  83. }
  84. function mapOptionCb<Value>(cb?: (item: TuiDialogSelectOption<Value>) => void) {
  85. if (!cb) return
  86. return (item: SelectOption<Value>) => cb(pickOption(item))
  87. }
  88. function stateApi(sync: ReturnType<typeof useSync>): TuiPluginApi["state"] {
  89. return {
  90. get ready() {
  91. return sync.ready
  92. },
  93. get config() {
  94. return sync.data.config
  95. },
  96. get provider() {
  97. return sync.data.provider
  98. },
  99. get path() {
  100. return sync.path
  101. },
  102. get vcs() {
  103. if (!sync.data.vcs) return
  104. return {
  105. branch: sync.data.vcs.branch,
  106. default_branch: sync.data.vcs.default_branch,
  107. }
  108. },
  109. session: {
  110. count() {
  111. return sync.data.session.length
  112. },
  113. get(sessionID) {
  114. return sync.session.get(sessionID)
  115. },
  116. diff(sessionID) {
  117. return (sync.data.session_diff[sessionID] ?? []).flatMap((item) =>
  118. item.file === undefined ? [] : [{ ...item, file: item.file }],
  119. )
  120. },
  121. todo(sessionID) {
  122. return sync.data.todo[sessionID] ?? []
  123. },
  124. messages(sessionID) {
  125. return sync.data.message[sessionID] ?? []
  126. },
  127. status(sessionID) {
  128. return sync.data.session_status[sessionID]
  129. },
  130. permission(sessionID) {
  131. return sync.data.permission[sessionID] ?? []
  132. },
  133. question(sessionID) {
  134. return sync.data.question[sessionID] ?? []
  135. },
  136. },
  137. part(messageID) {
  138. return sync.data.part[messageID] ?? []
  139. },
  140. lsp() {
  141. return sync.data.lsp.map((item) => ({ id: item.id, root: item.root, status: item.status }))
  142. },
  143. mcp() {
  144. return Object.entries(sync.data.mcp)
  145. .sort(([a], [b]) => a.localeCompare(b))
  146. .map(([name, item]) => ({
  147. name,
  148. status: item.status,
  149. error: item.status === "failed" ? item.error : undefined,
  150. }))
  151. },
  152. }
  153. }
  154. function appApi(version: string): TuiPluginApi["app"] {
  155. return {
  156. get version() {
  157. return version
  158. },
  159. }
  160. }
  161. export function createTuiApiAdapters(input: Input): Omit<TuiPluginApi, "lifecycle"> {
  162. return {
  163. app: appApi(input.version),
  164. attention: input.attention,
  165. // Keep deprecated `api.command` working for v1 plugins; remove in v2.
  166. command: createCommandShim(input.keymap, input.dialog, input.tuiConfig.keybinds),
  167. keys: {
  168. formatSequence(parts) {
  169. return Keymap.formatKeySequence(parts, input.tuiConfig)
  170. },
  171. formatBindings(bindings) {
  172. return Keymap.formatKeyBindings(bindings, input.tuiConfig)
  173. },
  174. },
  175. keymap: input.keymap,
  176. mode: {
  177. current() {
  178. return Keymap.getOpencodeModeStack(input.keymap).current()
  179. },
  180. push(mode) {
  181. return Keymap.getOpencodeModeStack(input.keymap).push(mode)
  182. },
  183. },
  184. route: {
  185. register(list) {
  186. return input.routes.register(list)
  187. },
  188. navigate(name, params) {
  189. routeNavigate(input.route, name, params)
  190. },
  191. get current() {
  192. return routeCurrent(input.route)
  193. },
  194. },
  195. ui: {
  196. Dialog(props) {
  197. return (
  198. <DialogUI size={props.size} onClose={props.onClose}>
  199. {props.children}
  200. </DialogUI>
  201. )
  202. },
  203. DialogAlert(props) {
  204. return <DialogAlert {...props} />
  205. },
  206. DialogConfirm(props) {
  207. return <DialogConfirm {...props} />
  208. },
  209. DialogPrompt(props) {
  210. return <DialogPrompt {...props} description={props.description} />
  211. },
  212. DialogSelect(props) {
  213. return (
  214. <DialogSelect
  215. title={props.title}
  216. placeholder={props.placeholder}
  217. options={props.options.map(mapOption)}
  218. flat={props.flat}
  219. onMove={mapOptionCb(props.onMove)}
  220. onFilter={props.onFilter}
  221. onSelect={mapOptionCb(props.onSelect)}
  222. skipFilter={props.skipFilter}
  223. current={props.current}
  224. />
  225. )
  226. },
  227. Slot<Name extends string>(props: TuiSlotProps<Name>) {
  228. return <input.Slot {...props} />
  229. },
  230. Prompt(props) {
  231. return (
  232. <Prompt
  233. sessionID={props.sessionID}
  234. visible={props.visible}
  235. disabled={props.disabled}
  236. onSubmit={props.onSubmit}
  237. ref={props.ref}
  238. hint={props.hint}
  239. right={props.right}
  240. showPlaceholder={props.showPlaceholder}
  241. placeholders={props.placeholders}
  242. />
  243. )
  244. },
  245. toast(inputToast) {
  246. input.toast.show({
  247. title: inputToast.title,
  248. message: inputToast.message,
  249. variant: inputToast.variant ?? "info",
  250. duration: inputToast.duration,
  251. })
  252. },
  253. dialog: {
  254. replace(render, onClose) {
  255. input.dialog.replace(render, onClose)
  256. },
  257. clear() {
  258. input.dialog.clear()
  259. },
  260. setSize(size) {
  261. input.dialog.setSize(size)
  262. },
  263. get size() {
  264. return input.dialog.size
  265. },
  266. get depth() {
  267. return input.dialog.stack.length
  268. },
  269. get open() {
  270. return input.dialog.stack.length > 0
  271. },
  272. },
  273. },
  274. get tuiConfig() {
  275. return input.tuiConfig
  276. },
  277. kv: {
  278. get(key, fallback) {
  279. return input.kv.get(key, fallback)
  280. },
  281. set(key, value) {
  282. input.kv.set(key, value)
  283. },
  284. get ready() {
  285. return input.kv.ready
  286. },
  287. },
  288. state: stateApi(input.sync),
  289. get client() {
  290. return input.sdk.client
  291. },
  292. event: input.event,
  293. renderer: input.renderer,
  294. slots: {
  295. register() {
  296. throw new Error("slots.register is only available in plugin context")
  297. },
  298. },
  299. plugins: {
  300. list() {
  301. return []
  302. },
  303. async activate() {
  304. return false
  305. },
  306. async deactivate() {
  307. return false
  308. },
  309. async add() {
  310. return false
  311. },
  312. async install() {
  313. return {
  314. ok: false,
  315. message: "plugins.install is only available in plugin context",
  316. }
  317. },
  318. },
  319. theme: {
  320. get current() {
  321. return input.theme.theme
  322. },
  323. get selected() {
  324. return input.theme.selected
  325. },
  326. has(name) {
  327. return input.theme.has(name)
  328. },
  329. set(name) {
  330. return input.theme.set(name)
  331. },
  332. async install(_jsonPath) {
  333. throw new Error("theme.install is only available in plugin context")
  334. },
  335. mode() {
  336. return input.theme.mode()
  337. },
  338. get ready() {
  339. return input.theme.ready
  340. },
  341. },
  342. }
  343. }