dialog-status.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import { TextAttributes } from "@opentui/core"
  2. import { useTheme } from "../context/theme"
  3. import { useDialog } from "@tui/ui/dialog"
  4. import { useSync } from "@tui/context/sync"
  5. import { For, Match, Switch, Show, createMemo, createSignal } from "solid-js"
  6. import { Installation } from "@/installation"
  7. export type DialogStatusProps = {}
  8. export function DialogStatus() {
  9. const sync = useSync()
  10. const { theme } = useTheme()
  11. const dialog = useDialog()
  12. const [hover, setHover] = createSignal(false)
  13. const enabledFormatters = createMemo(() => sync.data.formatter.filter((f) => f.enabled))
  14. const plugins = createMemo(() => {
  15. const list = sync.data.config.plugin ?? []
  16. const result = list.map((value) => {
  17. if (value.startsWith("file://")) {
  18. const path = value.substring("file://".length)
  19. const parts = path.split("/")
  20. const filename = parts.pop() || path
  21. if (!filename.includes(".")) return { name: filename }
  22. const basename = filename.split(".")[0]
  23. if (basename === "index") {
  24. const dirname = parts.pop()
  25. const name = dirname || basename
  26. return { name }
  27. }
  28. return { name: basename }
  29. }
  30. const index = value.lastIndexOf("@")
  31. if (index <= 0) return { name: value, version: "latest" }
  32. const name = value.substring(0, index)
  33. const version = value.substring(index + 1)
  34. return { name, version }
  35. })
  36. return result.toSorted((a, b) => a.name.localeCompare(b.name))
  37. })
  38. return (
  39. <box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
  40. <box flexDirection="row" justifyContent="space-between">
  41. <text fg={theme.text} attributes={TextAttributes.BOLD}>
  42. Status
  43. </text>
  44. <box
  45. paddingLeft={1}
  46. paddingRight={1}
  47. backgroundColor={hover() ? theme.primary : undefined}
  48. onMouseOver={() => setHover(true)}
  49. onMouseOut={() => setHover(false)}
  50. onMouseUp={() => dialog.clear()}
  51. >
  52. <text fg={hover() ? theme.selectedListItemText : theme.textMuted}>esc</text>
  53. </box>
  54. </box>
  55. <text fg={theme.textMuted}>OpenCode v{Installation.VERSION}</text>
  56. <Show when={Object.keys(sync.data.mcp).length > 0} fallback={<text fg={theme.text}>No MCP Servers</text>}>
  57. <box>
  58. <text fg={theme.text}>{Object.keys(sync.data.mcp).length} MCP Servers</text>
  59. <For each={Object.entries(sync.data.mcp)}>
  60. {([key, item]) => (
  61. <box flexDirection="row" gap={1}>
  62. <text
  63. flexShrink={0}
  64. style={{
  65. fg: (
  66. {
  67. connected: theme.success,
  68. failed: theme.error,
  69. disabled: theme.textMuted,
  70. needs_auth: theme.warning,
  71. needs_client_registration: theme.error,
  72. } as Record<string, typeof theme.success>
  73. )[item.status],
  74. }}
  75. >
  76. •
  77. </text>
  78. <text fg={theme.text} wrapMode="word">
  79. <b>{key}</b>{" "}
  80. <span style={{ fg: theme.textMuted }}>
  81. <Switch fallback={item.status}>
  82. <Match when={item.status === "connected"}>Connected</Match>
  83. <Match when={item.status === "failed" && item}>{(val) => val().error}</Match>
  84. <Match when={item.status === "disabled"}>Disabled in configuration</Match>
  85. <Match when={(item.status as string) === "needs_auth"}>
  86. Needs authentication (run: opencode mcp auth {key})
  87. </Match>
  88. <Match when={(item.status as string) === "needs_client_registration" && item}>
  89. {(val) => (val() as { error: string }).error}
  90. </Match>
  91. </Switch>
  92. </span>
  93. </text>
  94. </box>
  95. )}
  96. </For>
  97. </box>
  98. </Show>
  99. {sync.data.lsp.length > 0 && (
  100. <box>
  101. <text fg={theme.text}>{sync.data.lsp.length} LSP Servers</text>
  102. <For each={sync.data.lsp}>
  103. {(item) => (
  104. <box flexDirection="row" gap={1}>
  105. <text
  106. flexShrink={0}
  107. style={{
  108. fg: {
  109. connected: theme.success,
  110. error: theme.error,
  111. }[item.status],
  112. }}
  113. >
  114. •
  115. </text>
  116. <text fg={theme.text} wrapMode="word">
  117. <b>{item.id}</b> <span style={{ fg: theme.textMuted }}>{item.root}</span>
  118. </text>
  119. </box>
  120. )}
  121. </For>
  122. </box>
  123. )}
  124. <Show when={enabledFormatters().length > 0} fallback={<text fg={theme.text}>No Formatters</text>}>
  125. <box>
  126. <text fg={theme.text}>{enabledFormatters().length} Formatters</text>
  127. <For each={enabledFormatters()}>
  128. {(item) => (
  129. <box flexDirection="row" gap={1}>
  130. <text
  131. flexShrink={0}
  132. style={{
  133. fg: theme.success,
  134. }}
  135. >
  136. •
  137. </text>
  138. <text wrapMode="word" fg={theme.text}>
  139. <b>{item.name}</b>
  140. </text>
  141. </box>
  142. )}
  143. </For>
  144. </box>
  145. </Show>
  146. <Show when={plugins().length > 0} fallback={<text fg={theme.text}>No Plugins</text>}>
  147. <box>
  148. <text fg={theme.text}>{plugins().length} Plugins</text>
  149. <For each={plugins()}>
  150. {(item) => (
  151. <box flexDirection="row" gap={1}>
  152. <text
  153. flexShrink={0}
  154. style={{
  155. fg: theme.success,
  156. }}
  157. >
  158. •
  159. </text>
  160. <text wrapMode="word" fg={theme.text}>
  161. <b>{item.name}</b>
  162. {item.version && <span style={{ fg: theme.textMuted }}> @{item.version}</span>}
  163. </text>
  164. </box>
  165. )}
  166. </For>
  167. </box>
  168. </Show>
  169. </box>
  170. )
  171. }