tui.ts 13 KB

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