reigncode-app 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/AGENTS.md +30 -0
  2. package/Dockerfile +21 -0
  3. package/README.md +51 -0
  4. package/bunfig.toml +3 -0
  5. package/create-effect-simplification-spec.md +515 -0
  6. package/e2e/AGENTS.md +226 -0
  7. package/e2e/actions.ts +1018 -0
  8. package/e2e/app/home.spec.ts +24 -0
  9. package/e2e/app/navigation.spec.ts +10 -0
  10. package/e2e/app/palette.spec.ts +20 -0
  11. package/e2e/app/server-default.spec.ts +58 -0
  12. package/e2e/app/session.spec.ts +16 -0
  13. package/e2e/app/titlebar-history.spec.ts +120 -0
  14. package/e2e/commands/input-focus.spec.ts +15 -0
  15. package/e2e/commands/panels.spec.ts +33 -0
  16. package/e2e/commands/tab-close.spec.ts +32 -0
  17. package/e2e/files/file-open.spec.ts +31 -0
  18. package/e2e/files/file-tree.spec.ts +56 -0
  19. package/e2e/files/file-viewer.spec.ts +156 -0
  20. package/e2e/fixtures.ts +154 -0
  21. package/e2e/models/model-picker.spec.ts +48 -0
  22. package/e2e/models/models-visibility.spec.ts +61 -0
  23. package/e2e/projects/project-edit.spec.ts +43 -0
  24. package/e2e/projects/projects-close.spec.ts +54 -0
  25. package/e2e/projects/projects-switch.spec.ts +116 -0
  26. package/e2e/projects/workspace-new-session.spec.ts +94 -0
  27. package/e2e/projects/workspaces.spec.ts +375 -0
  28. package/e2e/prompt/context.spec.ts +95 -0
  29. package/e2e/prompt/prompt-async.spec.ts +76 -0
  30. package/e2e/prompt/prompt-drop-file-uri.spec.ts +22 -0
  31. package/e2e/prompt/prompt-drop-file.spec.ts +30 -0
  32. package/e2e/prompt/prompt-history.spec.ts +184 -0
  33. package/e2e/prompt/prompt-mention.spec.ts +26 -0
  34. package/e2e/prompt/prompt-multiline.spec.ts +24 -0
  35. package/e2e/prompt/prompt-shell.spec.ts +62 -0
  36. package/e2e/prompt/prompt-slash-open.spec.ts +22 -0
  37. package/e2e/prompt/prompt-slash-share.spec.ts +64 -0
  38. package/e2e/prompt/prompt-slash-terminal.spec.ts +18 -0
  39. package/e2e/prompt/prompt.spec.ts +55 -0
  40. package/e2e/selectors.ts +75 -0
  41. package/e2e/session/session-child-navigation.spec.ts +37 -0
  42. package/e2e/session/session-composer-dock.spec.ts +530 -0
  43. package/e2e/session/session-model-persistence.spec.ts +359 -0
  44. package/e2e/session/session-review.spec.ts +426 -0
  45. package/e2e/session/session-undo-redo.spec.ts +233 -0
  46. package/e2e/session/session.spec.ts +174 -0
  47. package/e2e/settings/settings-keybinds.spec.ts +389 -0
  48. package/e2e/settings/settings-models.spec.ts +122 -0
  49. package/e2e/settings/settings-providers.spec.ts +136 -0
  50. package/e2e/settings/settings.spec.ts +519 -0
  51. package/e2e/sidebar/sidebar-popover-actions.spec.ts +118 -0
  52. package/e2e/sidebar/sidebar-session-links.spec.ts +30 -0
  53. package/e2e/sidebar/sidebar.spec.ts +40 -0
  54. package/e2e/status/status-popover.spec.ts +94 -0
  55. package/e2e/terminal/terminal-init.spec.ts +28 -0
  56. package/e2e/terminal/terminal-reconnect.spec.ts +46 -0
  57. package/e2e/terminal/terminal-tabs.spec.ts +168 -0
  58. package/e2e/terminal/terminal.spec.ts +18 -0
  59. package/e2e/thinking-level.spec.ts +25 -0
  60. package/e2e/tsconfig.json +9 -0
  61. package/e2e/utils.ts +63 -0
  62. package/happydom.ts +75 -0
  63. package/index.html +23 -0
  64. package/package.json +77 -0
  65. package/playwright.config.ts +45 -0
  66. package/public/_headers +17 -0
  67. package/public/oc-theme-preload.js +35 -0
  68. package/script/e2e-local.ts +180 -0
  69. package/src/addons/serialize.test.ts +319 -0
  70. package/src/addons/serialize.ts +634 -0
  71. package/src/app.tsx +308 -0
  72. package/src/components/debug-bar.tsx +443 -0
  73. package/src/components/dialog-connect-provider.tsx +617 -0
  74. package/src/components/dialog-custom-provider-form.ts +158 -0
  75. package/src/components/dialog-custom-provider.test.ts +80 -0
  76. package/src/components/dialog-custom-provider.tsx +329 -0
  77. package/src/components/dialog-edit-project.tsx +255 -0
  78. package/src/components/dialog-fork.tsx +108 -0
  79. package/src/components/dialog-manage-models.tsx +101 -0
  80. package/src/components/dialog-release-notes.tsx +144 -0
  81. package/src/components/dialog-select-directory.tsx +392 -0
  82. package/src/components/dialog-select-file.tsx +466 -0
  83. package/src/components/dialog-select-mcp.tsx +107 -0
  84. package/src/components/dialog-select-model-unpaid.tsx +137 -0
  85. package/src/components/dialog-select-model.tsx +220 -0
  86. package/src/components/dialog-select-provider.tsx +86 -0
  87. package/src/components/dialog-select-server.tsx +649 -0
  88. package/src/components/dialog-settings.tsx +73 -0
  89. package/src/components/file-tree.test.ts +78 -0
  90. package/src/components/file-tree.tsx +507 -0
  91. package/src/components/link.tsx +26 -0
  92. package/src/components/model-tooltip.tsx +91 -0
  93. package/src/components/prompt-input/attachments.test.ts +44 -0
  94. package/src/components/prompt-input/attachments.ts +201 -0
  95. package/src/components/prompt-input/build-request-parts.test.ts +312 -0
  96. package/src/components/prompt-input/build-request-parts.ts +175 -0
  97. package/src/components/prompt-input/context-items.tsx +88 -0
  98. package/src/components/prompt-input/drag-overlay.tsx +25 -0
  99. package/src/components/prompt-input/editor-dom.test.ts +99 -0
  100. package/src/components/prompt-input/editor-dom.ts +148 -0
  101. package/src/components/prompt-input/files.ts +66 -0
  102. package/src/components/prompt-input/history.test.ts +153 -0
  103. package/src/components/prompt-input/history.ts +256 -0
  104. package/src/components/prompt-input/image-attachments.tsx +58 -0
  105. package/src/components/prompt-input/paste.ts +24 -0
  106. package/src/components/prompt-input/placeholder.test.ts +48 -0
  107. package/src/components/prompt-input/placeholder.ts +15 -0
  108. package/src/components/prompt-input/slash-popover.tsx +141 -0
  109. package/src/components/prompt-input/submit.test.ts +346 -0
  110. package/src/components/prompt-input/submit.ts +579 -0
  111. package/src/components/prompt-input.tsx +1595 -0
  112. package/src/components/server/server-row.tsx +130 -0
  113. package/src/components/session/index.ts +5 -0
  114. package/src/components/session/session-context-breakdown.test.ts +61 -0
  115. package/src/components/session/session-context-breakdown.ts +132 -0
  116. package/src/components/session/session-context-format.ts +20 -0
  117. package/src/components/session/session-context-metrics.test.ts +101 -0
  118. package/src/components/session/session-context-metrics.ts +82 -0
  119. package/src/components/session/session-context-tab.tsx +339 -0
  120. package/src/components/session/session-header.tsx +486 -0
  121. package/src/components/session/session-new-view.tsx +91 -0
  122. package/src/components/session/session-sortable-tab.tsx +70 -0
  123. package/src/components/session/session-sortable-terminal-tab.tsx +193 -0
  124. package/src/components/session-context-usage.tsx +122 -0
  125. package/src/components/settings-general.tsx +585 -0
  126. package/src/components/settings-keybinds.tsx +453 -0
  127. package/src/components/settings-list.tsx +5 -0
  128. package/src/components/settings-models.tsx +137 -0
  129. package/src/components/settings-providers.tsx +251 -0
  130. package/src/components/status-popover.tsx +419 -0
  131. package/src/components/terminal.tsx +653 -0
  132. package/src/components/titlebar-history.test.ts +63 -0
  133. package/src/components/titlebar-history.ts +57 -0
  134. package/src/components/titlebar.tsx +312 -0
  135. package/src/constants/file-picker.ts +89 -0
  136. package/src/context/command-keybind.test.ts +69 -0
  137. package/src/context/command.test.ts +25 -0
  138. package/src/context/command.tsx +437 -0
  139. package/src/context/comments.test.ts +186 -0
  140. package/src/context/comments.tsx +243 -0
  141. package/src/context/file/content-cache.ts +88 -0
  142. package/src/context/file/path.test.ts +360 -0
  143. package/src/context/file/path.ts +151 -0
  144. package/src/context/file/tree-store.ts +170 -0
  145. package/src/context/file/types.ts +41 -0
  146. package/src/context/file/view-cache.ts +146 -0
  147. package/src/context/file/watcher.test.ts +149 -0
  148. package/src/context/file/watcher.ts +53 -0
  149. package/src/context/file-content-eviction-accounting.test.ts +65 -0
  150. package/src/context/file.tsx +280 -0
  151. package/src/context/global-sdk.tsx +232 -0
  152. package/src/context/global-sync/bootstrap.ts +206 -0
  153. package/src/context/global-sync/child-store.test.ts +38 -0
  154. package/src/context/global-sync/child-store.ts +281 -0
  155. package/src/context/global-sync/event-reducer.test.ts +552 -0
  156. package/src/context/global-sync/event-reducer.ts +359 -0
  157. package/src/context/global-sync/eviction.ts +28 -0
  158. package/src/context/global-sync/queue.ts +83 -0
  159. package/src/context/global-sync/session-cache.test.ts +102 -0
  160. package/src/context/global-sync/session-cache.ts +62 -0
  161. package/src/context/global-sync/session-load.ts +25 -0
  162. package/src/context/global-sync/session-prefetch.test.ts +96 -0
  163. package/src/context/global-sync/session-prefetch.ts +100 -0
  164. package/src/context/global-sync/session-trim.test.ts +59 -0
  165. package/src/context/global-sync/session-trim.ts +56 -0
  166. package/src/context/global-sync/types.ts +133 -0
  167. package/src/context/global-sync/utils.ts +25 -0
  168. package/src/context/global-sync.test.ts +122 -0
  169. package/src/context/global-sync.tsx +408 -0
  170. package/src/context/highlights.tsx +233 -0
  171. package/src/context/language.tsx +248 -0
  172. package/src/context/layout-scroll.test.ts +64 -0
  173. package/src/context/layout-scroll.ts +126 -0
  174. package/src/context/layout.test.ts +69 -0
  175. package/src/context/layout.tsx +937 -0
  176. package/src/context/local.tsx +422 -0
  177. package/src/context/model-variant.test.ts +86 -0
  178. package/src/context/model-variant.ts +52 -0
  179. package/src/context/models.tsx +163 -0
  180. package/src/context/notification.tsx +373 -0
  181. package/src/context/permission-auto-respond.test.ts +102 -0
  182. package/src/context/permission-auto-respond.ts +51 -0
  183. package/src/context/permission.tsx +277 -0
  184. package/src/context/platform.tsx +99 -0
  185. package/src/context/prompt.tsx +297 -0
  186. package/src/context/sdk.tsx +49 -0
  187. package/src/context/server.tsx +295 -0
  188. package/src/context/settings.tsx +241 -0
  189. package/src/context/sync-optimistic.test.ts +123 -0
  190. package/src/context/sync.tsx +618 -0
  191. package/src/context/terminal-title.ts +51 -0
  192. package/src/context/terminal.test.ts +82 -0
  193. package/src/context/terminal.tsx +437 -0
  194. package/src/entry.tsx +144 -0
  195. package/src/env.d.ts +18 -0
  196. package/src/hooks/use-providers.ts +44 -0
  197. package/src/i18n/ar.ts +855 -0
  198. package/src/i18n/br.ts +867 -0
  199. package/src/i18n/bs.ts +943 -0
  200. package/src/i18n/da.ts +937 -0
  201. package/src/i18n/de.ts +879 -0
  202. package/src/i18n/en.ts +948 -0
  203. package/src/i18n/es.ts +950 -0
  204. package/src/i18n/fr.ts +878 -0
  205. package/src/i18n/ja.ts +861 -0
  206. package/src/i18n/ko.ts +860 -0
  207. package/src/i18n/no.ts +944 -0
  208. package/src/i18n/parity.test.ts +32 -0
  209. package/src/i18n/pl.ts +865 -0
  210. package/src/i18n/ru.ts +946 -0
  211. package/src/i18n/th.ts +933 -0
  212. package/src/i18n/tr.ts +952 -0
  213. package/src/i18n/zh.ts +930 -0
  214. package/src/i18n/zht.ts +925 -0
  215. package/src/index.css +29 -0
  216. package/src/index.ts +6 -0
  217. package/src/pages/directory-layout.tsx +88 -0
  218. package/src/pages/error.tsx +327 -0
  219. package/src/pages/home.tsx +131 -0
  220. package/src/pages/layout/deep-links.ts +50 -0
  221. package/src/pages/layout/helpers.test.ts +211 -0
  222. package/src/pages/layout/helpers.ts +98 -0
  223. package/src/pages/layout/inline-editor.tsx +126 -0
  224. package/src/pages/layout/sidebar-items.tsx +437 -0
  225. package/src/pages/layout/sidebar-project.tsx +384 -0
  226. package/src/pages/layout/sidebar-shell.tsx +125 -0
  227. package/src/pages/layout/sidebar-workspace.tsx +504 -0
  228. package/src/pages/layout.tsx +2509 -0
  229. package/src/pages/session/composer/index.ts +2 -0
  230. package/src/pages/session/composer/session-composer-region.tsx +255 -0
  231. package/src/pages/session/composer/session-composer-state.test.ts +128 -0
  232. package/src/pages/session/composer/session-composer-state.ts +249 -0
  233. package/src/pages/session/composer/session-followup-dock.tsx +109 -0
  234. package/src/pages/session/composer/session-permission-dock.tsx +74 -0
  235. package/src/pages/session/composer/session-question-dock.tsx +449 -0
  236. package/src/pages/session/composer/session-request-tree.ts +52 -0
  237. package/src/pages/session/composer/session-revert-dock.tsx +99 -0
  238. package/src/pages/session/composer/session-todo-dock.tsx +330 -0
  239. package/src/pages/session/file-tab-scroll.test.ts +40 -0
  240. package/src/pages/session/file-tab-scroll.ts +67 -0
  241. package/src/pages/session/file-tabs.tsx +456 -0
  242. package/src/pages/session/handoff.ts +36 -0
  243. package/src/pages/session/helpers.test.ts +181 -0
  244. package/src/pages/session/helpers.ts +198 -0
  245. package/src/pages/session/message-gesture.test.ts +62 -0
  246. package/src/pages/session/message-gesture.ts +21 -0
  247. package/src/pages/session/message-id-from-hash.ts +6 -0
  248. package/src/pages/session/message-timeline.tsx +1013 -0
  249. package/src/pages/session/review-tab.tsx +170 -0
  250. package/src/pages/session/session-layout.ts +20 -0
  251. package/src/pages/session/session-model-helpers.test.ts +51 -0
  252. package/src/pages/session/session-model-helpers.ts +16 -0
  253. package/src/pages/session/session-side-panel.tsx +453 -0
  254. package/src/pages/session/terminal-label.ts +16 -0
  255. package/src/pages/session/terminal-panel.test.ts +25 -0
  256. package/src/pages/session/terminal-panel.tsx +326 -0
  257. package/src/pages/session/use-session-commands.tsx +495 -0
  258. package/src/pages/session/use-session-hash-scroll.test.ts +16 -0
  259. package/src/pages/session/use-session-hash-scroll.ts +197 -0
  260. package/src/pages/session.tsx +1841 -0
  261. package/src/sst-env.d.ts +12 -0
  262. package/src/testing/model-selection.ts +80 -0
  263. package/src/testing/prompt.ts +56 -0
  264. package/src/testing/session-composer.ts +84 -0
  265. package/src/testing/terminal.ts +118 -0
  266. package/src/theme-preload.test.ts +46 -0
  267. package/src/utils/agent.ts +23 -0
  268. package/src/utils/aim.ts +138 -0
  269. package/src/utils/base64.ts +10 -0
  270. package/src/utils/comment-note.ts +88 -0
  271. package/src/utils/id.ts +99 -0
  272. package/src/utils/notification-click.test.ts +27 -0
  273. package/src/utils/notification-click.ts +13 -0
  274. package/src/utils/persist.test.ts +115 -0
  275. package/src/utils/persist.ts +476 -0
  276. package/src/utils/prompt.test.ts +44 -0
  277. package/src/utils/prompt.ts +203 -0
  278. package/src/utils/runtime-adapters.test.ts +62 -0
  279. package/src/utils/runtime-adapters.ts +39 -0
  280. package/src/utils/same.ts +6 -0
  281. package/src/utils/scoped-cache.test.ts +69 -0
  282. package/src/utils/scoped-cache.ts +104 -0
  283. package/src/utils/server-errors.test.ts +131 -0
  284. package/src/utils/server-errors.ts +80 -0
  285. package/src/utils/server-health.test.ts +123 -0
  286. package/src/utils/server-health.ts +91 -0
  287. package/src/utils/server.ts +22 -0
  288. package/src/utils/solid-dnd.tsx +49 -0
  289. package/src/utils/sound.ts +117 -0
  290. package/src/utils/terminal-writer.test.ts +64 -0
  291. package/src/utils/terminal-writer.ts +65 -0
  292. package/src/utils/time.ts +22 -0
  293. package/src/utils/uuid.test.ts +78 -0
  294. package/src/utils/uuid.ts +12 -0
  295. package/src/utils/worktree.test.ts +46 -0
  296. package/src/utils/worktree.ts +73 -0
  297. package/sst-env.d.ts +10 -0
  298. package/tsconfig.json +26 -0
  299. package/vite.config.ts +15 -0
  300. package/vite.js +26 -0
@@ -0,0 +1,251 @@
1
+ import { Button } from "@reign-labs/ui/button"
2
+ import { useDialog } from "@reign-labs/ui/context/dialog"
3
+ import { ProviderIcon } from "@reign-labs/ui/provider-icon"
4
+ import { Tag } from "@reign-labs/ui/tag"
5
+ import { showToast } from "@reign-labs/ui/toast"
6
+ import { popularProviders, useProviders } from "@/hooks/use-providers"
7
+ import { createMemo, type Component, For, Show } from "solid-js"
8
+ import { useLanguage } from "@/context/language"
9
+ import { useGlobalSDK } from "@/context/global-sdk"
10
+ import { useGlobalSync } from "@/context/global-sync"
11
+ import { DialogConnectProvider } from "./dialog-connect-provider"
12
+ import { DialogSelectProvider } from "./dialog-select-provider"
13
+ import { DialogCustomProvider } from "./dialog-custom-provider"
14
+ import { SettingsList } from "./settings-list"
15
+
16
+ type ProviderSource = "env" | "api" | "config" | "custom"
17
+ type ProviderItem = ReturnType<ReturnType<typeof useProviders>["connected"]>[number]
18
+
19
+ const PROVIDER_NOTES = [
20
+ { match: (id: string) => id === "opencode", key: "dialog.provider.opencode.note" },
21
+ { match: (id: string) => id === "opencode-go", key: "dialog.provider.opencodeGo.tagline" },
22
+ { match: (id: string) => id === "anthropic", key: "dialog.provider.anthropic.note" },
23
+ { match: (id: string) => id.startsWith("github-copilot"), key: "dialog.provider.copilot.note" },
24
+ { match: (id: string) => id === "openai", key: "dialog.provider.openai.note" },
25
+ { match: (id: string) => id === "google", key: "dialog.provider.google.note" },
26
+ { match: (id: string) => id === "openrouter", key: "dialog.provider.openrouter.note" },
27
+ { match: (id: string) => id === "vercel", key: "dialog.provider.vercel.note" },
28
+ ] as const
29
+
30
+ export const SettingsProviders: Component = () => {
31
+ const dialog = useDialog()
32
+ const language = useLanguage()
33
+ const globalSDK = useGlobalSDK()
34
+ const globalSync = useGlobalSync()
35
+ const providers = useProviders()
36
+
37
+ const connected = createMemo(() => {
38
+ return providers
39
+ .connected()
40
+ .filter((p) => p.id !== "opencode" || Object.values(p.models).find((m) => m.cost?.input))
41
+ })
42
+
43
+ const popular = createMemo(() => {
44
+ const connectedIDs = new Set(connected().map((p) => p.id))
45
+ const items = providers
46
+ .popular()
47
+ .filter((p) => !connectedIDs.has(p.id))
48
+ .slice()
49
+ items.sort((a, b) => popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id))
50
+ return items
51
+ })
52
+
53
+ const source = (item: ProviderItem): ProviderSource | undefined => {
54
+ if (!("source" in item)) return
55
+ const value = item.source
56
+ if (value === "env" || value === "api" || value === "config" || value === "custom") return value
57
+ return
58
+ }
59
+
60
+ const type = (item: ProviderItem) => {
61
+ const current = source(item)
62
+ if (current === "env") return language.t("settings.providers.tag.environment")
63
+ if (current === "api") return language.t("provider.connect.method.apiKey")
64
+ if (current === "config") {
65
+ if (isConfigCustom(item.id)) return language.t("settings.providers.tag.custom")
66
+ return language.t("settings.providers.tag.config")
67
+ }
68
+ if (current === "custom") return language.t("settings.providers.tag.custom")
69
+ return language.t("settings.providers.tag.other")
70
+ }
71
+
72
+ const canDisconnect = (item: ProviderItem) => source(item) !== "env"
73
+
74
+ const note = (id: string) => PROVIDER_NOTES.find((item) => item.match(id))?.key
75
+
76
+ const isConfigCustom = (providerID: string) => {
77
+ const provider = globalSync.data.config.provider?.[providerID]
78
+ if (!provider) return false
79
+ if (provider.npm !== "@ai-sdk/openai-compatible") return false
80
+ if (!provider.models || Object.keys(provider.models).length === 0) return false
81
+ return true
82
+ }
83
+
84
+ const disableProvider = async (providerID: string, name: string) => {
85
+ const before = globalSync.data.config.disabled_providers ?? []
86
+ const next = before.includes(providerID) ? before : [...before, providerID]
87
+ globalSync.set("config", "disabled_providers", next)
88
+
89
+ await globalSync
90
+ .updateConfig({ disabled_providers: next })
91
+ .then(() => {
92
+ showToast({
93
+ variant: "success",
94
+ icon: "circle-check",
95
+ title: language.t("provider.disconnect.toast.disconnected.title", { provider: name }),
96
+ description: language.t("provider.disconnect.toast.disconnected.description", { provider: name }),
97
+ })
98
+ })
99
+ .catch((err: unknown) => {
100
+ globalSync.set("config", "disabled_providers", before)
101
+ const message = err instanceof Error ? err.message : String(err)
102
+ showToast({ title: language.t("common.requestFailed"), description: message })
103
+ })
104
+ }
105
+
106
+ const disconnect = async (providerID: string, name: string) => {
107
+ if (isConfigCustom(providerID)) {
108
+ await globalSDK.client.auth.remove({ providerID }).catch(() => undefined)
109
+ await disableProvider(providerID, name)
110
+ return
111
+ }
112
+ await globalSDK.client.auth
113
+ .remove({ providerID })
114
+ .then(async () => {
115
+ await globalSDK.client.global.dispose()
116
+ showToast({
117
+ variant: "success",
118
+ icon: "circle-check",
119
+ title: language.t("provider.disconnect.toast.disconnected.title", { provider: name }),
120
+ description: language.t("provider.disconnect.toast.disconnected.description", { provider: name }),
121
+ })
122
+ })
123
+ .catch((err: unknown) => {
124
+ const message = err instanceof Error ? err.message : String(err)
125
+ showToast({ title: language.t("common.requestFailed"), description: message })
126
+ })
127
+ }
128
+
129
+ return (
130
+ <div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
131
+ <div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
132
+ <div class="flex flex-col gap-1 pt-6 pb-8 max-w-[720px]">
133
+ <h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
134
+ </div>
135
+ </div>
136
+
137
+ <div class="flex flex-col gap-8 max-w-[720px]">
138
+ <div class="flex flex-col gap-1" data-component="connected-providers-section">
139
+ <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.providers.section.connected")}</h3>
140
+ <SettingsList>
141
+ <Show
142
+ when={connected().length > 0}
143
+ fallback={
144
+ <div class="py-4 text-14-regular text-text-weak">
145
+ {language.t("settings.providers.connected.empty")}
146
+ </div>
147
+ }
148
+ >
149
+ <For each={connected()}>
150
+ {(item) => (
151
+ <div class="group flex flex-wrap items-center justify-between gap-4 min-h-16 py-3 border-b border-border-weak-base last:border-none">
152
+ <div class="flex items-center gap-3 min-w-0">
153
+ <ProviderIcon id={item.id} class="size-5 shrink-0 icon-strong-base" />
154
+ <span class="text-14-medium text-text-strong truncate">{item.name}</span>
155
+ <Tag>{type(item)}</Tag>
156
+ </div>
157
+ <Show
158
+ when={canDisconnect(item)}
159
+ fallback={
160
+ <span class="text-14-regular text-text-base opacity-0 group-hover:opacity-100 transition-opacity duration-200 pr-3 cursor-default">
161
+ {language.t("settings.providers.connected.environmentDescription")}
162
+ </span>
163
+ }
164
+ >
165
+ <Button size="large" variant="ghost" onClick={() => void disconnect(item.id, item.name)}>
166
+ {language.t("common.disconnect")}
167
+ </Button>
168
+ </Show>
169
+ </div>
170
+ )}
171
+ </For>
172
+ </Show>
173
+ </SettingsList>
174
+ </div>
175
+
176
+ <div class="flex flex-col gap-1">
177
+ <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.providers.section.popular")}</h3>
178
+ <SettingsList>
179
+ <For each={popular()}>
180
+ {(item) => (
181
+ <div class="flex flex-wrap items-center justify-between gap-4 min-h-16 py-3 border-b border-border-weak-base last:border-none">
182
+ <div class="flex flex-col min-w-0">
183
+ <div class="flex items-center gap-x-3">
184
+ <ProviderIcon id={item.id} class="size-5 shrink-0 icon-strong-base" />
185
+ <span class="text-14-medium text-text-strong">{item.name}</span>
186
+ <Show when={item.id === "opencode"}>
187
+ <Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
188
+ </Show>
189
+ <Show when={item.id === "opencode-go"}>
190
+ <Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
191
+ </Show>
192
+ </div>
193
+ <Show when={note(item.id)}>
194
+ {(key) => <span class="text-12-regular text-text-weak pl-8">{language.t(key())}</span>}
195
+ </Show>
196
+ </div>
197
+ <Button
198
+ size="large"
199
+ variant="secondary"
200
+ icon="plus-small"
201
+ onClick={() => {
202
+ dialog.show(() => <DialogConnectProvider provider={item.id} />)
203
+ }}
204
+ >
205
+ {language.t("common.connect")}
206
+ </Button>
207
+ </div>
208
+ )}
209
+ </For>
210
+
211
+ <div
212
+ class="flex items-center justify-between gap-4 min-h-16 border-b border-border-weak-base last:border-none flex-wrap py-3"
213
+ data-component="custom-provider-section"
214
+ >
215
+ <div class="flex flex-col min-w-0">
216
+ <div class="flex flex-wrap items-center gap-x-3 gap-y-1">
217
+ <ProviderIcon id="synthetic" class="size-5 shrink-0 icon-strong-base" />
218
+ <span class="text-14-medium text-text-strong">{language.t("provider.custom.title")}</span>
219
+ <Tag>{language.t("settings.providers.tag.custom")}</Tag>
220
+ </div>
221
+ <span class="text-12-regular text-text-weak pl-8">
222
+ {language.t("settings.providers.custom.description")}
223
+ </span>
224
+ </div>
225
+ <Button
226
+ size="large"
227
+ variant="secondary"
228
+ icon="plus-small"
229
+ onClick={() => {
230
+ dialog.show(() => <DialogCustomProvider back="close" />)
231
+ }}
232
+ >
233
+ {language.t("common.connect")}
234
+ </Button>
235
+ </div>
236
+ </SettingsList>
237
+
238
+ <Button
239
+ variant="ghost"
240
+ class="px-0 py-0 mt-5 text-14-medium text-text-interactive-base text-left justify-start hover:bg-transparent active:bg-transparent"
241
+ onClick={() => {
242
+ dialog.show(() => <DialogSelectProvider />)
243
+ }}
244
+ >
245
+ {language.t("dialog.provider.viewAll")}
246
+ </Button>
247
+ </div>
248
+ </div>
249
+ </div>
250
+ )
251
+ }
@@ -0,0 +1,419 @@
1
+ import { Button } from "@reign-labs/ui/button"
2
+ import { useDialog } from "@reign-labs/ui/context/dialog"
3
+ import { Icon } from "@reign-labs/ui/icon"
4
+ import { Popover } from "@reign-labs/ui/popover"
5
+ import { Switch } from "@reign-labs/ui/switch"
6
+ import { Tabs } from "@reign-labs/ui/tabs"
7
+ import { useMutation } from "@tanstack/solid-query"
8
+ import { showToast } from "@reign-labs/ui/toast"
9
+ import { useNavigate } from "@solidjs/router"
10
+ import { type Accessor, createEffect, createMemo, createSignal, For, type JSXElement, onCleanup, Show } from "solid-js"
11
+ import { createStore, reconcile } from "solid-js/store"
12
+ import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
13
+ import { useLanguage } from "@/context/language"
14
+ import { usePlatform } from "@/context/platform"
15
+ import { useSDK } from "@/context/sdk"
16
+ import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
17
+ import { useSync } from "@/context/sync"
18
+ import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health"
19
+ import { DialogSelectServer } from "./dialog-select-server"
20
+
21
+ const pollMs = 10_000
22
+
23
+ const pluginEmptyMessage = (value: string, file: string): JSXElement => {
24
+ const parts = value.split(file)
25
+ if (parts.length === 1) return value
26
+ return (
27
+ <>
28
+ {parts[0]}
29
+ <code class="bg-surface-raised-base px-1.5 py-0.5 rounded-sm text-text-base">{file}</code>
30
+ {parts.slice(1).join(file)}
31
+ </>
32
+ )
33
+ }
34
+
35
+ const listServersByHealth = (
36
+ list: ServerConnection.Any[],
37
+ active: ServerConnection.Key | undefined,
38
+ status: Record<ServerConnection.Key, ServerHealth | undefined>,
39
+ ) => {
40
+ if (!list.length) return list
41
+ const order = new Map(list.map((url, index) => [url, index] as const))
42
+ const rank = (value?: ServerHealth) => {
43
+ if (value?.healthy === true) return 0
44
+ if (value?.healthy === false) return 2
45
+ return 1
46
+ }
47
+
48
+ return list.slice().sort((a, b) => {
49
+ if (ServerConnection.key(a) === active) return -1
50
+ if (ServerConnection.key(b) === active) return 1
51
+ const diff = rank(status[ServerConnection.key(a)]) - rank(status[ServerConnection.key(b)])
52
+ if (diff !== 0) return diff
53
+ return (order.get(a) ?? 0) - (order.get(b) ?? 0)
54
+ })
55
+ }
56
+
57
+ const useServerHealth = (servers: Accessor<ServerConnection.Any[]>) => {
58
+ const checkServerHealth = useCheckServerHealth()
59
+ const [status, setStatus] = createStore({} as Record<ServerConnection.Key, ServerHealth | undefined>)
60
+
61
+ createEffect(() => {
62
+ const list = servers()
63
+ let dead = false
64
+
65
+ const refresh = async () => {
66
+ const results: Record<string, ServerHealth> = {}
67
+ await Promise.all(
68
+ list.map(async (conn) => {
69
+ results[ServerConnection.key(conn)] = await checkServerHealth(conn.http)
70
+ }),
71
+ )
72
+ if (dead) return
73
+ setStatus(reconcile(results))
74
+ }
75
+
76
+ void refresh()
77
+ const id = setInterval(() => void refresh(), pollMs)
78
+ onCleanup(() => {
79
+ dead = true
80
+ clearInterval(id)
81
+ })
82
+ })
83
+
84
+ return status
85
+ }
86
+
87
+ const useDefaultServerKey = (
88
+ get: (() => string | Promise<string | null | undefined> | null | undefined) | undefined,
89
+ ) => {
90
+ const [state, setState] = createStore({
91
+ url: undefined as string | undefined,
92
+ tick: 0,
93
+ })
94
+
95
+ createEffect(() => {
96
+ state.tick
97
+ let dead = false
98
+ const result = get?.()
99
+ if (!result) {
100
+ setState("url", undefined)
101
+ onCleanup(() => {
102
+ dead = true
103
+ })
104
+ return
105
+ }
106
+
107
+ if (result instanceof Promise) {
108
+ void result.then((next) => {
109
+ if (dead) return
110
+ setState("url", next ? normalizeServerUrl(next) : undefined)
111
+ })
112
+ onCleanup(() => {
113
+ dead = true
114
+ })
115
+ return
116
+ }
117
+
118
+ setState("url", normalizeServerUrl(result))
119
+ onCleanup(() => {
120
+ dead = true
121
+ })
122
+ })
123
+
124
+ return {
125
+ key: () => {
126
+ const u = state.url
127
+ if (!u) return
128
+ return ServerConnection.key({ type: "http", http: { url: u } })
129
+ },
130
+ refresh: () => setState("tick", (value) => value + 1),
131
+ }
132
+ }
133
+
134
+ const useMcpToggleMutation = () => {
135
+ const sync = useSync()
136
+ const sdk = useSDK()
137
+ const language = useLanguage()
138
+
139
+ return useMutation(() => ({
140
+ mutationFn: async (name: string) => {
141
+ const status = sync.data.mcp[name]
142
+ await (status?.status === "connected" ? sdk.client.mcp.disconnect({ name }) : sdk.client.mcp.connect({ name }))
143
+ const result = await sdk.client.mcp.status()
144
+ if (result.data) sync.set("mcp", result.data)
145
+ },
146
+ onError: (err) => {
147
+ showToast({
148
+ variant: "error",
149
+ title: language.t("common.requestFailed"),
150
+ description: err instanceof Error ? err.message : String(err),
151
+ })
152
+ },
153
+ }))
154
+ }
155
+
156
+ export function StatusPopover() {
157
+ const sync = useSync()
158
+ const server = useServer()
159
+ const platform = usePlatform()
160
+ const dialog = useDialog()
161
+ const language = useLanguage()
162
+ const navigate = useNavigate()
163
+
164
+ const [shown, setShown] = createSignal(false)
165
+ const servers = createMemo(() => {
166
+ const current = server.current
167
+ const list = server.list
168
+ if (!current) return list
169
+ if (list.every((item) => ServerConnection.key(item) !== ServerConnection.key(current))) return [current, ...list]
170
+ return [current, ...list.filter((item) => ServerConnection.key(item) !== ServerConnection.key(current))]
171
+ })
172
+ const health = useServerHealth(servers)
173
+ const sortedServers = createMemo(() => listServersByHealth(servers(), server.key, health))
174
+ const toggleMcp = useMcpToggleMutation()
175
+ const defaultServer = useDefaultServerKey(platform.getDefaultServer)
176
+ const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b)))
177
+ const mcpStatus = (name: string) => sync.data.mcp?.[name]?.status
178
+ const mcpConnected = createMemo(() => mcpNames().filter((name) => mcpStatus(name) === "connected").length)
179
+ const lspItems = createMemo(() => sync.data.lsp ?? [])
180
+ const lspCount = createMemo(() => lspItems().length)
181
+ const plugins = createMemo(() => sync.data.config.plugin ?? [])
182
+ const pluginCount = createMemo(() => plugins().length)
183
+ const pluginEmpty = createMemo(() => pluginEmptyMessage(language.t("dialog.plugins.empty"), "reigncode.json"))
184
+ const overallHealthy = createMemo(() => {
185
+ const serverHealthy = server.healthy() === true
186
+ const anyMcpIssue = mcpNames().some((name) => {
187
+ const status = mcpStatus(name)
188
+ return status !== "connected" && status !== "disabled"
189
+ })
190
+ return serverHealthy && !anyMcpIssue
191
+ })
192
+
193
+ return (
194
+ <Popover
195
+ open={shown()}
196
+ onOpenChange={setShown}
197
+ triggerAs={Button}
198
+ triggerProps={{
199
+ variant: "ghost",
200
+ class: "titlebar-icon w-8 h-6 p-0 box-border",
201
+ "aria-label": language.t("status.popover.trigger"),
202
+ style: { scale: 1 },
203
+ }}
204
+ trigger={
205
+ <div class="relative size-4">
206
+ <div class="badge-mask-tight size-4 flex items-center justify-center">
207
+ <Icon name={shown() ? "status-active" : "status"} size="small" />
208
+ </div>
209
+ <div
210
+ classList={{
211
+ "absolute -top-px -right-px size-1.5 rounded-full": true,
212
+ "bg-icon-success-base": overallHealthy(),
213
+ "bg-icon-critical-base": !overallHealthy() && server.healthy() !== undefined,
214
+ "bg-border-weak-base": server.healthy() === undefined,
215
+ }}
216
+ />
217
+ </div>
218
+ }
219
+ class="[&_[data-slot=popover-body]]:p-0 w-[360px] max-w-[calc(100vw-40px)] bg-transparent border-0 shadow-none rounded-xl"
220
+ gutter={4}
221
+ placement="bottom-end"
222
+ shift={-168}
223
+ >
224
+ <div class="flex items-center gap-1 w-[360px] rounded-xl shadow-[var(--shadow-lg-border-base)]">
225
+ <Tabs
226
+ aria-label={language.t("status.popover.ariaLabel")}
227
+ class="tabs bg-background-strong rounded-xl overflow-hidden"
228
+ data-component="tabs"
229
+ data-active="servers"
230
+ defaultValue="servers"
231
+ variant="alt"
232
+ >
233
+ <Tabs.List data-slot="tablist" class="bg-transparent border-b-0 px-4 pt-2 pb-0 gap-4 h-10">
234
+ <Tabs.Trigger value="servers" data-slot="tab" class="text-12-regular">
235
+ {sortedServers().length > 0 ? `${sortedServers().length} ` : ""}
236
+ {language.t("status.popover.tab.servers")}
237
+ </Tabs.Trigger>
238
+ <Tabs.Trigger value="mcp" data-slot="tab" class="text-12-regular">
239
+ {mcpConnected() > 0 ? `${mcpConnected()} ` : ""}
240
+ {language.t("status.popover.tab.mcp")}
241
+ </Tabs.Trigger>
242
+ <Tabs.Trigger value="lsp" data-slot="tab" class="text-12-regular">
243
+ {lspCount() > 0 ? `${lspCount()} ` : ""}
244
+ {language.t("status.popover.tab.lsp")}
245
+ </Tabs.Trigger>
246
+ <Tabs.Trigger value="plugins" data-slot="tab" class="text-12-regular">
247
+ {pluginCount() > 0 ? `${pluginCount()} ` : ""}
248
+ {language.t("status.popover.tab.plugins")}
249
+ </Tabs.Trigger>
250
+ </Tabs.List>
251
+
252
+ <Tabs.Content value="servers">
253
+ <div class="flex flex-col px-2 pb-2">
254
+ <div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
255
+ <For each={sortedServers()}>
256
+ {(s) => {
257
+ const key = ServerConnection.key(s)
258
+ const isBlocked = () => health[key]?.healthy === false
259
+ return (
260
+ <button
261
+ type="button"
262
+ class="flex items-center gap-2 w-full h-8 pl-3 pr-1.5 py-1.5 rounded-md transition-colors text-left"
263
+ classList={{
264
+ "hover:bg-surface-raised-base-hover": !isBlocked(),
265
+ "cursor-not-allowed": isBlocked(),
266
+ }}
267
+ aria-disabled={isBlocked()}
268
+ onClick={() => {
269
+ if (isBlocked()) return
270
+ navigate("/")
271
+ queueMicrotask(() => server.setActive(key))
272
+ }}
273
+ >
274
+ <ServerHealthIndicator health={health[key]} />
275
+ <ServerRow
276
+ conn={s}
277
+ dimmed={isBlocked()}
278
+ status={health[key]}
279
+ class="flex items-center gap-2 w-full min-w-0"
280
+ nameClass="text-14-regular text-text-base truncate"
281
+ versionClass="text-12-regular text-text-weak truncate"
282
+ badge={
283
+ <Show when={key === defaultServer.key()}>
284
+ <span class="text-11-regular text-text-base bg-surface-base px-1.5 py-0.5 rounded-md">
285
+ {language.t("common.default")}
286
+ </span>
287
+ </Show>
288
+ }
289
+ >
290
+ <div class="flex-1" />
291
+ <Show when={server.current && key === ServerConnection.key(server.current)}>
292
+ <Icon name="check" size="small" class="text-icon-weak shrink-0" />
293
+ </Show>
294
+ </ServerRow>
295
+ </button>
296
+ )
297
+ }}
298
+ </For>
299
+
300
+ <Button
301
+ variant="secondary"
302
+ class="mt-3 self-start h-8 px-3 py-1.5"
303
+ onClick={() => dialog.show(() => <DialogSelectServer />, defaultServer.refresh)}
304
+ >
305
+ {language.t("status.popover.action.manageServers")}
306
+ </Button>
307
+ </div>
308
+ </div>
309
+ </Tabs.Content>
310
+
311
+ <Tabs.Content value="mcp">
312
+ <div class="flex flex-col px-2 pb-2">
313
+ <div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
314
+ <Show
315
+ when={mcpNames().length > 0}
316
+ fallback={
317
+ <div class="text-14-regular text-text-base text-center my-auto">
318
+ {language.t("dialog.mcp.empty")}
319
+ </div>
320
+ }
321
+ >
322
+ <For each={mcpNames()}>
323
+ {(name) => {
324
+ const status = () => mcpStatus(name)
325
+ const enabled = () => status() === "connected"
326
+ return (
327
+ <button
328
+ type="button"
329
+ class="flex items-center gap-2 w-full h-8 pl-3 pr-2 py-1 rounded-md hover:bg-surface-raised-base-hover transition-colors text-left"
330
+ onClick={() => {
331
+ if (toggleMcp.isPending) return
332
+ toggleMcp.mutate(name)
333
+ }}
334
+ disabled={toggleMcp.isPending && toggleMcp.variables === name}
335
+ >
336
+ <div
337
+ classList={{
338
+ "size-1.5 rounded-full shrink-0": true,
339
+ "bg-icon-success-base": status() === "connected",
340
+ "bg-icon-critical-base": status() === "failed",
341
+ "bg-border-weak-base": status() === "disabled",
342
+ "bg-icon-warning-base":
343
+ status() === "needs_auth" || status() === "needs_client_registration",
344
+ }}
345
+ />
346
+ <span class="text-14-regular text-text-base truncate flex-1">{name}</span>
347
+ <div onClick={(event) => event.stopPropagation()}>
348
+ <Switch
349
+ checked={enabled()}
350
+ disabled={toggleMcp.isPending && toggleMcp.variables === name}
351
+ onChange={() => {
352
+ if (toggleMcp.isPending) return
353
+ toggleMcp.mutate(name)
354
+ }}
355
+ />
356
+ </div>
357
+ </button>
358
+ )
359
+ }}
360
+ </For>
361
+ </Show>
362
+ </div>
363
+ </div>
364
+ </Tabs.Content>
365
+
366
+ <Tabs.Content value="lsp">
367
+ <div class="flex flex-col px-2 pb-2">
368
+ <div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
369
+ <Show
370
+ when={lspItems().length > 0}
371
+ fallback={
372
+ <div class="text-14-regular text-text-base text-center my-auto">
373
+ {language.t("dialog.lsp.empty")}
374
+ </div>
375
+ }
376
+ >
377
+ <For each={lspItems()}>
378
+ {(item) => (
379
+ <div class="flex items-center gap-2 w-full px-2 py-1">
380
+ <div
381
+ classList={{
382
+ "size-1.5 rounded-full shrink-0": true,
383
+ "bg-icon-success-base": item.status === "connected",
384
+ "bg-icon-critical-base": item.status === "error",
385
+ }}
386
+ />
387
+ <span class="text-14-regular text-text-base truncate">{item.name || item.id}</span>
388
+ </div>
389
+ )}
390
+ </For>
391
+ </Show>
392
+ </div>
393
+ </div>
394
+ </Tabs.Content>
395
+
396
+ <Tabs.Content value="plugins">
397
+ <div class="flex flex-col px-2 pb-2">
398
+ <div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
399
+ <Show
400
+ when={plugins().length > 0}
401
+ fallback={<div class="text-14-regular text-text-base text-center my-auto">{pluginEmpty()}</div>}
402
+ >
403
+ <For each={plugins()}>
404
+ {(plugin) => (
405
+ <div class="flex items-center gap-2 w-full px-2 py-1">
406
+ <div class="size-1.5 rounded-full shrink-0 bg-icon-success-base" />
407
+ <span class="text-14-regular text-text-base truncate">{plugin}</span>
408
+ </div>
409
+ )}
410
+ </For>
411
+ </Show>
412
+ </div>
413
+ </div>
414
+ </Tabs.Content>
415
+ </Tabs>
416
+ </div>
417
+ </Popover>
418
+ )
419
+ }