tui.ts 14 KB

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