tui.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. import type {
  2. OpencodeClient,
  3. Event,
  4. LspStatus,
  5. McpStatus,
  6. Todo,
  7. Message,
  8. Part,
  9. Provider,
  10. PermissionRequest,
  11. QuestionRequest,
  12. SessionStatus,
  13. Workspace,
  14. Config as SdkConfig,
  15. } from "@opencode-ai/sdk/v2"
  16. import type { CliRenderer, ParsedKey, RGBA } from "@opentui/core"
  17. import type { JSX, SolidPlugin } from "@opentui/solid"
  18. import type { Config as PluginConfig, PluginOptions } from "./index.js"
  19. export type { CliRenderer, SlotMode } from "@opentui/core"
  20. export type TuiRouteCurrent =
  21. | {
  22. name: "home"
  23. }
  24. | {
  25. name: "session"
  26. params: {
  27. sessionID: string
  28. initialPrompt?: unknown
  29. }
  30. }
  31. | {
  32. name: string
  33. params?: Record<string, unknown>
  34. }
  35. export type TuiRouteDefinition = {
  36. name: string
  37. render: (input: { params?: Record<string, unknown> }) => JSX.Element
  38. }
  39. export type TuiCommand = {
  40. title: string
  41. value: string
  42. description?: string
  43. category?: string
  44. keybind?: string
  45. suggested?: boolean
  46. hidden?: boolean
  47. enabled?: boolean
  48. slash?: {
  49. name: string
  50. aliases?: string[]
  51. }
  52. onSelect?: () => void
  53. }
  54. export type TuiKeybind = {
  55. name: string
  56. ctrl: boolean
  57. meta: boolean
  58. shift: boolean
  59. super?: boolean
  60. leader: boolean
  61. }
  62. export type TuiKeybindMap = Record<string, string>
  63. export type TuiKeybindSet = {
  64. readonly all: TuiKeybindMap
  65. get: (name: string) => string
  66. match: (name: string, evt: ParsedKey) => boolean
  67. print: (name: string) => string
  68. }
  69. export type TuiDialogProps = {
  70. size?: "medium" | "large" | "xlarge"
  71. onClose: () => void
  72. children?: JSX.Element
  73. }
  74. export type TuiDialogStack = {
  75. replace: (render: () => JSX.Element, onClose?: () => void) => void
  76. clear: () => void
  77. setSize: (size: "medium" | "large" | "xlarge") => void
  78. readonly size: "medium" | "large" | "xlarge"
  79. readonly depth: number
  80. readonly open: boolean
  81. }
  82. export type TuiDialogAlertProps = {
  83. title: string
  84. message: string
  85. onConfirm?: () => void
  86. }
  87. export type TuiDialogConfirmProps = {
  88. title: string
  89. message: string
  90. onConfirm?: () => void
  91. onCancel?: () => void
  92. }
  93. export type TuiDialogPromptProps = {
  94. title: string
  95. description?: () => JSX.Element
  96. placeholder?: string
  97. value?: string
  98. busy?: boolean
  99. busyText?: string
  100. onConfirm?: (value: string) => void
  101. onCancel?: () => void
  102. }
  103. export type TuiDialogSelectOption<Value = unknown> = {
  104. title: string
  105. value: Value
  106. description?: string
  107. footer?: JSX.Element | string
  108. category?: string
  109. disabled?: boolean
  110. onSelect?: () => void
  111. }
  112. export type TuiDialogSelectProps<Value = unknown> = {
  113. title: string
  114. placeholder?: string
  115. options: TuiDialogSelectOption<Value>[]
  116. flat?: boolean
  117. onMove?: (option: TuiDialogSelectOption<Value>) => void
  118. onFilter?: (query: string) => void
  119. onSelect?: (option: TuiDialogSelectOption<Value>) => void
  120. skipFilter?: boolean
  121. current?: Value
  122. }
  123. export type TuiPromptProps = {
  124. workspaceID?: string
  125. visible?: boolean
  126. disabled?: boolean
  127. onSubmit?: () => void
  128. hint?: JSX.Element
  129. showPlaceholder?: boolean
  130. placeholders?: {
  131. normal?: string[]
  132. shell?: string[]
  133. }
  134. }
  135. export type TuiToast = {
  136. variant?: "info" | "success" | "warning" | "error"
  137. title?: string
  138. message: string
  139. duration?: number
  140. }
  141. export type TuiThemeCurrent = {
  142. readonly primary: RGBA
  143. readonly secondary: RGBA
  144. readonly accent: RGBA
  145. readonly error: RGBA
  146. readonly warning: RGBA
  147. readonly success: RGBA
  148. readonly info: RGBA
  149. readonly text: RGBA
  150. readonly textMuted: RGBA
  151. readonly selectedListItemText: RGBA
  152. readonly background: RGBA
  153. readonly backgroundPanel: RGBA
  154. readonly backgroundElement: RGBA
  155. readonly backgroundMenu: RGBA
  156. readonly border: RGBA
  157. readonly borderActive: RGBA
  158. readonly borderSubtle: RGBA
  159. readonly diffAdded: RGBA
  160. readonly diffRemoved: RGBA
  161. readonly diffContext: RGBA
  162. readonly diffHunkHeader: RGBA
  163. readonly diffHighlightAdded: RGBA
  164. readonly diffHighlightRemoved: RGBA
  165. readonly diffAddedBg: RGBA
  166. readonly diffRemovedBg: RGBA
  167. readonly diffContextBg: RGBA
  168. readonly diffLineNumber: RGBA
  169. readonly diffAddedLineNumberBg: RGBA
  170. readonly diffRemovedLineNumberBg: RGBA
  171. readonly markdownText: RGBA
  172. readonly markdownHeading: RGBA
  173. readonly markdownLink: RGBA
  174. readonly markdownLinkText: RGBA
  175. readonly markdownCode: RGBA
  176. readonly markdownBlockQuote: RGBA
  177. readonly markdownEmph: RGBA
  178. readonly markdownStrong: RGBA
  179. readonly markdownHorizontalRule: RGBA
  180. readonly markdownListItem: RGBA
  181. readonly markdownListEnumeration: RGBA
  182. readonly markdownImage: RGBA
  183. readonly markdownImageText: RGBA
  184. readonly markdownCodeBlock: RGBA
  185. readonly syntaxComment: RGBA
  186. readonly syntaxKeyword: RGBA
  187. readonly syntaxFunction: RGBA
  188. readonly syntaxVariable: RGBA
  189. readonly syntaxString: RGBA
  190. readonly syntaxNumber: RGBA
  191. readonly syntaxType: RGBA
  192. readonly syntaxOperator: RGBA
  193. readonly syntaxPunctuation: RGBA
  194. readonly thinkingOpacity: number
  195. }
  196. export type TuiTheme = {
  197. readonly current: TuiThemeCurrent
  198. readonly selected: string
  199. has: (name: string) => boolean
  200. set: (name: string) => boolean
  201. install: (jsonPath: string) => Promise<void>
  202. mode: () => "dark" | "light"
  203. readonly ready: boolean
  204. }
  205. export type TuiKV = {
  206. get: <Value = unknown>(key: string, fallback?: Value) => Value
  207. set: (key: string, value: unknown) => void
  208. readonly ready: boolean
  209. }
  210. export type TuiState = {
  211. readonly ready: boolean
  212. readonly config: SdkConfig
  213. readonly provider: ReadonlyArray<Provider>
  214. readonly path: {
  215. state: string
  216. config: string
  217. worktree: string
  218. directory: string
  219. }
  220. readonly vcs: { branch?: string } | undefined
  221. readonly workspace: {
  222. list: () => ReadonlyArray<Workspace>
  223. get: (workspaceID: string) => Workspace | undefined
  224. }
  225. session: {
  226. count: () => number
  227. diff: (sessionID: string) => ReadonlyArray<TuiSidebarFileItem>
  228. todo: (sessionID: string) => ReadonlyArray<TuiSidebarTodoItem>
  229. messages: (sessionID: string) => ReadonlyArray<Message>
  230. status: (sessionID: string) => SessionStatus | undefined
  231. permission: (sessionID: string) => ReadonlyArray<PermissionRequest>
  232. question: (sessionID: string) => ReadonlyArray<QuestionRequest>
  233. }
  234. part: (messageID: string) => ReadonlyArray<Part>
  235. lsp: () => ReadonlyArray<TuiSidebarLspItem>
  236. mcp: () => ReadonlyArray<TuiSidebarMcpItem>
  237. }
  238. type TuiConfigView = Pick<PluginConfig, "$schema" | "theme" | "keybinds" | "plugin"> &
  239. NonNullable<PluginConfig["tui"]> & {
  240. plugin_enabled?: Record<string, boolean>
  241. }
  242. export type TuiApp = {
  243. readonly version: string
  244. }
  245. type Frozen<Value> = Value extends (...args: never[]) => unknown
  246. ? Value
  247. : Value extends ReadonlyArray<infer Item>
  248. ? ReadonlyArray<Frozen<Item>>
  249. : Value extends object
  250. ? { readonly [Key in keyof Value]: Frozen<Value[Key]> }
  251. : Value
  252. export type TuiSidebarMcpItem = {
  253. name: string
  254. status: McpStatus["status"]
  255. error?: string
  256. }
  257. export type TuiSidebarLspItem = Pick<LspStatus, "id" | "root" | "status">
  258. export type TuiSidebarTodoItem = Pick<Todo, "content" | "status">
  259. export type TuiSidebarFileItem = {
  260. file: string
  261. additions: number
  262. deletions: number
  263. }
  264. export type TuiSlotMap = {
  265. app: {}
  266. home_logo: {}
  267. home_prompt: {
  268. workspace_id?: string
  269. }
  270. home_bottom: {}
  271. home_footer: {}
  272. sidebar_title: {
  273. session_id: string
  274. title: string
  275. share_url?: string
  276. }
  277. sidebar_content: {
  278. session_id: string
  279. }
  280. sidebar_footer: {
  281. session_id: string
  282. }
  283. }
  284. export type TuiSlotContext = {
  285. theme: TuiTheme
  286. }
  287. type SlotCore = SolidPlugin<TuiSlotMap, TuiSlotContext>
  288. export type TuiSlotPlugin = Omit<SlotCore, "id"> & {
  289. id?: never
  290. }
  291. export type TuiSlots = {
  292. register: (plugin: TuiSlotPlugin) => string
  293. }
  294. export type TuiEventBus = {
  295. on: <Type extends Event["type"]>(type: Type, handler: (event: Extract<Event, { type: Type }>) => void) => () => void
  296. }
  297. export type TuiDispose = () => void | Promise<void>
  298. export type TuiLifecycle = {
  299. readonly signal: AbortSignal
  300. onDispose: (fn: TuiDispose) => () => void
  301. }
  302. export type TuiPluginState = "first" | "updated" | "same"
  303. export type TuiPluginEntry = {
  304. id: string
  305. source: "file" | "npm" | "internal"
  306. spec: string
  307. target: string
  308. requested?: string
  309. version?: string
  310. modified?: number
  311. first_time: number
  312. last_time: number
  313. time_changed: number
  314. load_count: number
  315. fingerprint: string
  316. }
  317. export type TuiPluginMeta = TuiPluginEntry & {
  318. state: TuiPluginState
  319. }
  320. export type TuiPluginStatus = {
  321. id: string
  322. source: TuiPluginEntry["source"]
  323. spec: string
  324. target: string
  325. enabled: boolean
  326. active: boolean
  327. }
  328. export type TuiPluginInstallOptions = {
  329. global?: boolean
  330. }
  331. export type TuiPluginInstallResult =
  332. | {
  333. ok: true
  334. dir: string
  335. tui: boolean
  336. }
  337. | {
  338. ok: false
  339. message: string
  340. missing?: boolean
  341. }
  342. export type TuiWorkspace = {
  343. current: () => string | undefined
  344. set: (workspaceID?: string) => void
  345. }
  346. export type TuiPluginApi = {
  347. app: TuiApp
  348. command: {
  349. register: (cb: () => TuiCommand[]) => () => void
  350. trigger: (value: string) => void
  351. }
  352. route: {
  353. register: (routes: TuiRouteDefinition[]) => () => void
  354. navigate: (name: string, params?: Record<string, unknown>) => void
  355. readonly current: TuiRouteCurrent
  356. }
  357. ui: {
  358. Dialog: (props: TuiDialogProps) => JSX.Element
  359. DialogAlert: (props: TuiDialogAlertProps) => JSX.Element
  360. DialogConfirm: (props: TuiDialogConfirmProps) => JSX.Element
  361. DialogPrompt: (props: TuiDialogPromptProps) => JSX.Element
  362. DialogSelect: <Value = unknown>(props: TuiDialogSelectProps<Value>) => JSX.Element
  363. Prompt: (props: TuiPromptProps) => JSX.Element
  364. toast: (input: TuiToast) => void
  365. dialog: TuiDialogStack
  366. }
  367. keybind: {
  368. match: (key: string, evt: ParsedKey) => boolean
  369. print: (key: string) => string
  370. create: (defaults: TuiKeybindMap, overrides?: Record<string, unknown>) => TuiKeybindSet
  371. }
  372. readonly tuiConfig: Frozen<TuiConfigView>
  373. kv: TuiKV
  374. state: TuiState
  375. theme: TuiTheme
  376. client: OpencodeClient
  377. scopedClient: (workspaceID?: string) => OpencodeClient
  378. workspace: TuiWorkspace
  379. event: TuiEventBus
  380. renderer: CliRenderer
  381. slots: TuiSlots
  382. plugins: {
  383. list: () => ReadonlyArray<TuiPluginStatus>
  384. activate: (id: string) => Promise<boolean>
  385. deactivate: (id: string) => Promise<boolean>
  386. add: (spec: string) => Promise<boolean>
  387. install: (spec: string, options?: TuiPluginInstallOptions) => Promise<TuiPluginInstallResult>
  388. }
  389. lifecycle: TuiLifecycle
  390. }
  391. export type TuiPlugin = (api: TuiPluginApi, options: PluginOptions | undefined, meta: TuiPluginMeta) => Promise<void>
  392. export type TuiPluginModule = {
  393. id?: string
  394. tui: TuiPlugin
  395. server?: never
  396. }