tui.ts 16 KB

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