tui.ts 14 KB

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