tui-plugin.ts 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. import { createKiloClient } from "@kilocode/sdk/v2"
  2. import { RGBA, type CliRenderer } from "@opentui/core"
  3. import { createPluginKeybind } from "../../src/cli/cmd/tui/context/plugin-keybinds"
  4. import type { HostPluginApi } from "../../src/cli/cmd/tui/plugin/slots"
  5. type Count = {
  6. event_add: number
  7. event_drop: number
  8. route_add: number
  9. route_drop: number
  10. command_add: number
  11. command_drop: number
  12. }
  13. function themeCurrent(): HostPluginApi["theme"]["current"] {
  14. const a = RGBA.fromInts(0, 120, 240)
  15. const b = RGBA.fromInts(120, 120, 120)
  16. const c = RGBA.fromInts(230, 230, 230)
  17. const d = RGBA.fromInts(120, 30, 30)
  18. const e = RGBA.fromInts(140, 100, 40)
  19. const f = RGBA.fromInts(20, 140, 80)
  20. const g = RGBA.fromInts(20, 80, 160)
  21. const h = RGBA.fromInts(40, 40, 40)
  22. const i = RGBA.fromInts(60, 60, 60)
  23. const j = RGBA.fromInts(80, 80, 80)
  24. return {
  25. primary: a,
  26. secondary: b,
  27. accent: a,
  28. error: d,
  29. warning: e,
  30. success: f,
  31. info: g,
  32. text: c,
  33. textMuted: b,
  34. selectedListItemText: h,
  35. background: h,
  36. backgroundPanel: h,
  37. backgroundElement: i,
  38. backgroundMenu: i,
  39. border: j,
  40. borderActive: c,
  41. borderSubtle: i,
  42. diffAdded: f,
  43. diffRemoved: d,
  44. diffContext: b,
  45. diffHunkHeader: b,
  46. diffHighlightAdded: f,
  47. diffHighlightRemoved: d,
  48. diffAddedBg: h,
  49. diffRemovedBg: h,
  50. diffContextBg: h,
  51. diffLineNumber: b,
  52. diffAddedLineNumberBg: h,
  53. diffRemovedLineNumberBg: h,
  54. markdownText: c,
  55. markdownHeading: c,
  56. markdownLink: a,
  57. markdownLinkText: g,
  58. markdownCode: f,
  59. markdownBlockQuote: e,
  60. markdownEmph: e,
  61. markdownStrong: c,
  62. markdownHorizontalRule: b,
  63. markdownListItem: a,
  64. markdownListEnumeration: g,
  65. markdownImage: a,
  66. markdownImageText: g,
  67. markdownCodeBlock: c,
  68. syntaxComment: b,
  69. syntaxKeyword: a,
  70. syntaxFunction: g,
  71. syntaxVariable: c,
  72. syntaxString: f,
  73. syntaxNumber: e,
  74. syntaxType: a,
  75. syntaxOperator: a,
  76. syntaxPunctuation: c,
  77. thinkingOpacity: 0.6,
  78. }
  79. }
  80. type Opts = {
  81. client?: HostPluginApi["client"] | (() => HostPluginApi["client"])
  82. renderer?: HostPluginApi["renderer"]
  83. count?: Count
  84. keybind?: Partial<HostPluginApi["keybind"]>
  85. tuiConfig?: HostPluginApi["tuiConfig"]
  86. app?: Partial<HostPluginApi["app"]>
  87. state?: {
  88. ready?: HostPluginApi["state"]["ready"]
  89. config?: HostPluginApi["state"]["config"]
  90. provider?: HostPluginApi["state"]["provider"]
  91. path?: HostPluginApi["state"]["path"]
  92. vcs?: HostPluginApi["state"]["vcs"]
  93. workspace?: Partial<HostPluginApi["state"]["workspace"]>
  94. session?: Partial<HostPluginApi["state"]["session"]>
  95. part?: HostPluginApi["state"]["part"]
  96. lsp?: HostPluginApi["state"]["lsp"]
  97. mcp?: HostPluginApi["state"]["mcp"]
  98. }
  99. theme?: {
  100. selected?: string
  101. has?: HostPluginApi["theme"]["has"]
  102. set?: HostPluginApi["theme"]["set"]
  103. install?: HostPluginApi["theme"]["install"]
  104. mode?: HostPluginApi["theme"]["mode"]
  105. ready?: boolean
  106. current?: HostPluginApi["theme"]["current"]
  107. }
  108. }
  109. export function createTuiPluginApi(opts: Opts = {}): HostPluginApi {
  110. const kv: Record<string, unknown> = {}
  111. const count = opts.count
  112. const ctrl = new AbortController()
  113. const own = createKiloClient({
  114. baseUrl: "http://localhost:4096",
  115. })
  116. const fallback = () => own
  117. const read =
  118. typeof opts.client === "function"
  119. ? opts.client
  120. : opts.client
  121. ? () => opts.client as HostPluginApi["client"]
  122. : fallback
  123. const client = () => read()
  124. let depth = 0
  125. let size: "medium" | "large" | "xlarge" = "medium"
  126. const has = opts.theme?.has ?? (() => false)
  127. let selected = opts.theme?.selected ?? "opencode"
  128. const key = {
  129. match: opts.keybind?.match ?? (() => false),
  130. print: opts.keybind?.print ?? ((name: string) => name),
  131. }
  132. const set =
  133. opts.theme?.set ??
  134. ((name: string) => {
  135. if (!has(name)) return false
  136. selected = name
  137. return true
  138. })
  139. const renderer: CliRenderer = opts.renderer ?? {
  140. ...Object.create(null),
  141. once(this: CliRenderer) {
  142. return this
  143. },
  144. }
  145. function kvGet(name: string): unknown
  146. function kvGet<Value>(name: string, fallback: Value): Value
  147. function kvGet(name: string, fallback?: unknown) {
  148. const value = kv[name]
  149. if (value === undefined) return fallback
  150. return value
  151. }
  152. return {
  153. app: {
  154. get version() {
  155. return opts.app?.version ?? "0.0.0-test"
  156. },
  157. },
  158. get client() {
  159. return client()
  160. },
  161. event: {
  162. on: () => {
  163. if (count) count.event_add += 1
  164. return () => {
  165. if (!count) return
  166. count.event_drop += 1
  167. }
  168. },
  169. },
  170. renderer,
  171. slots: {
  172. register: () => "fixture-slot",
  173. },
  174. plugins: {
  175. list: () => [],
  176. activate: async () => false,
  177. deactivate: async () => false,
  178. add: async () => false,
  179. install: async () => ({
  180. ok: false,
  181. message: "not implemented in fixture",
  182. }),
  183. },
  184. lifecycle: {
  185. signal: ctrl.signal,
  186. onDispose() {
  187. return () => {}
  188. },
  189. },
  190. command: {
  191. register: () => {
  192. if (count) count.command_add += 1
  193. return () => {
  194. if (!count) return
  195. count.command_drop += 1
  196. }
  197. },
  198. trigger: () => {},
  199. show: () => {},
  200. },
  201. route: {
  202. register: () => {
  203. if (count) count.route_add += 1
  204. return () => {
  205. if (!count) return
  206. count.route_drop += 1
  207. }
  208. },
  209. navigate: () => {},
  210. get current() {
  211. return { name: "home" }
  212. },
  213. },
  214. ui: {
  215. Dialog: () => null,
  216. DialogAlert: () => null,
  217. DialogConfirm: () => null,
  218. DialogPrompt: () => null,
  219. DialogSelect: () => null,
  220. Slot: () => null,
  221. Prompt: () => null,
  222. toast: () => {},
  223. dialog: {
  224. replace: () => {
  225. depth = 1
  226. },
  227. clear: () => {
  228. depth = 0
  229. size = "medium"
  230. },
  231. setSize: (next) => {
  232. size = next
  233. },
  234. get size() {
  235. return size
  236. },
  237. get depth() {
  238. return depth
  239. },
  240. get open() {
  241. return depth > 0
  242. },
  243. },
  244. },
  245. keybind: {
  246. ...key,
  247. create:
  248. opts.keybind?.create ??
  249. ((defaults, over) => {
  250. return createPluginKeybind(key, defaults, over)
  251. }),
  252. },
  253. tuiConfig: opts.tuiConfig ?? {},
  254. kv: {
  255. get: kvGet,
  256. set(name, value) {
  257. kv[name] = value
  258. },
  259. get ready() {
  260. return true
  261. },
  262. },
  263. state: {
  264. get ready() {
  265. return opts.state?.ready ?? true
  266. },
  267. get config() {
  268. return opts.state?.config ?? {}
  269. },
  270. get provider() {
  271. return opts.state?.provider ?? []
  272. },
  273. get path() {
  274. return opts.state?.path ?? { state: "", config: "", worktree: "", directory: "" }
  275. },
  276. get vcs() {
  277. return opts.state?.vcs
  278. },
  279. workspace: {
  280. list: opts.state?.workspace?.list ?? (() => []),
  281. get: opts.state?.workspace?.get ?? (() => undefined),
  282. },
  283. session: {
  284. count: opts.state?.session?.count ?? (() => 0),
  285. diff: opts.state?.session?.diff ?? (() => []),
  286. todo: opts.state?.session?.todo ?? (() => []),
  287. messages: opts.state?.session?.messages ?? (() => []),
  288. status: opts.state?.session?.status ?? (() => undefined),
  289. permission: opts.state?.session?.permission ?? (() => []),
  290. question: opts.state?.session?.question ?? (() => []),
  291. },
  292. part: opts.state?.part ?? (() => []),
  293. lsp: opts.state?.lsp ?? (() => []),
  294. mcp: opts.state?.mcp ?? (() => []),
  295. },
  296. theme: {
  297. get current() {
  298. return opts.theme?.current ?? themeCurrent()
  299. },
  300. get selected() {
  301. return selected
  302. },
  303. has(name) {
  304. return has(name)
  305. },
  306. set(name) {
  307. return set(name)
  308. },
  309. async install(file) {
  310. if (opts.theme?.install) return opts.theme.install(file)
  311. throw new Error("base theme.install should not run")
  312. },
  313. mode() {
  314. if (opts.theme?.mode) return opts.theme.mode()
  315. return "dark"
  316. },
  317. get ready() {
  318. return opts.theme?.ready ?? true
  319. },
  320. },
  321. }
  322. }