dialog-status.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. import { TextAttributes } from "@opentui/core"
  2. import { fileURLToPath } from "bun"
  3. import { useTheme } from "../context/theme"
  4. import { useDialog } from "@tui/ui/dialog"
  5. import { useSync } from "@tui/context/sync"
  6. import { For, Match, Switch, Show, createMemo } from "solid-js"
  7. import { Installation } from "@/installation"
  8. export type DialogStatusProps = {}
  9. export function DialogStatus() {
  10. const sync = useSync()
  11. const { theme } = useTheme()
  12. const dialog = useDialog()
  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 = fileURLToPath(value)
  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. <text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
  45. esc
  46. </text>
  47. </box>
  48. <text fg={theme.textMuted}>OpenCode v{Installation.VERSION}</text>
  49. <Show when={Object.keys(sync.data.mcp).length > 0} fallback={<text fg={theme.text}>No MCP Servers</text>}>
  50. <box>
  51. <text fg={theme.text}>{Object.keys(sync.data.mcp).length} MCP Servers</text>
  52. <For each={Object.entries(sync.data.mcp)}>
  53. {([key, item]) => (
  54. <box flexDirection="row" gap={1}>
  55. <text
  56. flexShrink={0}
  57. style={{
  58. fg: (
  59. {
  60. connected: theme.success,
  61. failed: theme.error,
  62. disabled: theme.textMuted,
  63. needs_auth: theme.warning,
  64. needs_client_registration: theme.error,
  65. } as Record<string, typeof theme.success>
  66. )[item.status],
  67. }}
  68. >
  69. </text>
  70. <text fg={theme.text} wrapMode="word">
  71. <b>{key}</b>{" "}
  72. <span style={{ fg: theme.textMuted }}>
  73. <Switch fallback={item.status}>
  74. <Match when={item.status === "connected"}>Connected</Match>
  75. <Match when={item.status === "failed" && item}>{(val) => val().error}</Match>
  76. <Match when={item.status === "disabled"}>Disabled in configuration</Match>
  77. <Match when={(item.status as string) === "needs_auth"}>
  78. Needs authentication (run: opencode mcp auth {key})
  79. </Match>
  80. <Match when={(item.status as string) === "needs_client_registration" && item}>
  81. {(val) => (val() as { error: string }).error}
  82. </Match>
  83. </Switch>
  84. </span>
  85. </text>
  86. </box>
  87. )}
  88. </For>
  89. </box>
  90. </Show>
  91. {sync.data.lsp.length > 0 && (
  92. <box>
  93. <text fg={theme.text}>{sync.data.lsp.length} LSP Servers</text>
  94. <For each={sync.data.lsp}>
  95. {(item) => (
  96. <box flexDirection="row" gap={1}>
  97. <text
  98. flexShrink={0}
  99. style={{
  100. fg: {
  101. connected: theme.success,
  102. error: theme.error,
  103. }[item.status],
  104. }}
  105. >
  106. </text>
  107. <text fg={theme.text} wrapMode="word">
  108. <b>{item.id}</b> <span style={{ fg: theme.textMuted }}>{item.root}</span>
  109. </text>
  110. </box>
  111. )}
  112. </For>
  113. </box>
  114. )}
  115. <Show when={enabledFormatters().length > 0} fallback={<text fg={theme.text}>No Formatters</text>}>
  116. <box>
  117. <text fg={theme.text}>{enabledFormatters().length} Formatters</text>
  118. <For each={enabledFormatters()}>
  119. {(item) => (
  120. <box flexDirection="row" gap={1}>
  121. <text
  122. flexShrink={0}
  123. style={{
  124. fg: theme.success,
  125. }}
  126. >
  127. </text>
  128. <text wrapMode="word" fg={theme.text}>
  129. <b>{item.name}</b>
  130. </text>
  131. </box>
  132. )}
  133. </For>
  134. </box>
  135. </Show>
  136. <Show when={plugins().length > 0} fallback={<text fg={theme.text}>No Plugins</text>}>
  137. <box>
  138. <text fg={theme.text}>{plugins().length} Plugins</text>
  139. <For each={plugins()}>
  140. {(item) => (
  141. <box flexDirection="row" gap={1}>
  142. <text
  143. flexShrink={0}
  144. style={{
  145. fg: theme.success,
  146. }}
  147. >
  148. </text>
  149. <text wrapMode="word" fg={theme.text}>
  150. <b>{item.name}</b>
  151. {item.version && <span style={{ fg: theme.textMuted }}> @{item.version}</span>}
  152. </text>
  153. </box>
  154. )}
  155. </For>
  156. </box>
  157. </Show>
  158. </box>
  159. )
  160. }