tui.ts 12 KB

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