tui.ts 12 KB

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