tui.ts 16 KB

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