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
package/src/index.css ADDED
@@ -0,0 +1,29 @@
1
+ @import "@reign-labs/ui/styles/tailwind";
2
+
3
+ @layer components {
4
+ [data-component="getting-started"] {
5
+ container-type: inline-size;
6
+ container-name: getting-started;
7
+ }
8
+
9
+ [data-component="getting-started-actions"] {
10
+ display: flex;
11
+ flex-direction: column;
12
+ gap: 0.75rem; /* gap-3 */
13
+ }
14
+
15
+ [data-component="getting-started-actions"] > [data-component="button"] {
16
+ width: 100%;
17
+ }
18
+
19
+ @container getting-started (min-width: 17rem) {
20
+ [data-component="getting-started-actions"] {
21
+ flex-direction: row;
22
+ align-items: center;
23
+ }
24
+
25
+ [data-component="getting-started-actions"] > [data-component="button"] {
26
+ width: auto;
27
+ }
28
+ }
29
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { AppBaseProviders, AppInterface } from "./app"
2
+ export { ACCEPTED_FILE_EXTENSIONS, ACCEPTED_FILE_TYPES, filePickerFilters } from "./constants/file-picker"
3
+ export { useCommand } from "./context/command"
4
+ export { type DisplayBackend, type Platform, PlatformProvider } from "./context/platform"
5
+ export { ServerConnection } from "./context/server"
6
+ export { handleNotificationClick } from "./utils/notification-click"
@@ -0,0 +1,88 @@
1
+ import { DataProvider } from "@reign-labs/ui/context"
2
+ import { showToast } from "@reign-labs/ui/toast"
3
+ import { base64Encode } from "@reign-labs/util/encode"
4
+ import { useLocation, useNavigate, useParams } from "@solidjs/router"
5
+ import { createMemo, createResource, type ParentProps, Show } from "solid-js"
6
+ import { useGlobalSDK } from "@/context/global-sdk"
7
+ import { useLanguage } from "@/context/language"
8
+ import { LocalProvider } from "@/context/local"
9
+ import { SDKProvider } from "@/context/sdk"
10
+ import { SyncProvider, useSync } from "@/context/sync"
11
+ import { decode64 } from "@/utils/base64"
12
+
13
+ function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
14
+ const navigate = useNavigate()
15
+ const sync = useSync()
16
+ const slug = createMemo(() => base64Encode(props.directory))
17
+
18
+ return (
19
+ <DataProvider
20
+ data={sync.data}
21
+ directory={props.directory}
22
+ onNavigateToSession={(sessionID: string) => navigate(`/${slug()}/session/${sessionID}`)}
23
+ onSessionHref={(sessionID: string) => `/${slug()}/session/${sessionID}`}
24
+ >
25
+ <LocalProvider>{props.children}</LocalProvider>
26
+ </DataProvider>
27
+ )
28
+ }
29
+
30
+ export default function Layout(props: ParentProps) {
31
+ const params = useParams()
32
+ const location = useLocation()
33
+ const language = useLanguage()
34
+ const globalSDK = useGlobalSDK()
35
+ const navigate = useNavigate()
36
+ let invalid = ""
37
+
38
+ const [resolved] = createResource(
39
+ () => {
40
+ if (params.dir) return [location.pathname, params.dir] as const
41
+ },
42
+ async ([pathname, b64Dir]) => {
43
+ const directory = decode64(b64Dir)
44
+
45
+ if (!directory) {
46
+ if (invalid === params.dir) return
47
+ invalid = b64Dir
48
+ showToast({
49
+ variant: "error",
50
+ title: language.t("common.requestFailed"),
51
+ description: language.t("directory.error.invalidUrl"),
52
+ })
53
+ navigate("/", { replace: true })
54
+ return
55
+ }
56
+
57
+ return await globalSDK
58
+ .createClient({
59
+ directory,
60
+ throwOnError: true,
61
+ })
62
+ .path.get()
63
+ .then((x) => {
64
+ const next = x.data?.directory ?? directory
65
+ invalid = ""
66
+ if (next === directory) return next
67
+ const path = pathname.slice(b64Dir.length + 1)
68
+ navigate(`/${base64Encode(next)}${path}${location.search}${location.hash}`, { replace: true })
69
+ })
70
+ .catch(() => {
71
+ invalid = ""
72
+ return directory
73
+ })
74
+ },
75
+ )
76
+
77
+ return (
78
+ <Show when={resolved()} keyed>
79
+ {(resolved) => (
80
+ <SDKProvider directory={() => resolved}>
81
+ <SyncProvider>
82
+ <DirectoryDataProvider directory={resolved}>{props.children}</DirectoryDataProvider>
83
+ </SyncProvider>
84
+ </SDKProvider>
85
+ )}
86
+ </Show>
87
+ )
88
+ }
@@ -0,0 +1,327 @@
1
+ import { TextField } from "@reign-labs/ui/text-field"
2
+ import { Logo } from "@reign-labs/ui/logo"
3
+ import { Button } from "@reign-labs/ui/button"
4
+ import { Component, Show, onMount } from "solid-js"
5
+ import { createStore } from "solid-js/store"
6
+ import { usePlatform } from "@/context/platform"
7
+ import { useLanguage } from "@/context/language"
8
+ import { Icon } from "@reign-labs/ui/icon"
9
+ import type { E2EWindow } from "@/testing/terminal"
10
+
11
+ export type InitError = {
12
+ name: string
13
+ data: Record<string, unknown>
14
+ }
15
+
16
+ type Translator = ReturnType<typeof useLanguage>["t"]
17
+ const CHAIN_SEPARATOR = "\n" + "─".repeat(40) + "\n"
18
+
19
+ function isIssue(value: unknown): value is { message: string; path: string[] } {
20
+ if (!value || typeof value !== "object") return false
21
+ if (!("message" in value) || !("path" in value)) return false
22
+ const message = (value as { message: unknown }).message
23
+ const path = (value as { path: unknown }).path
24
+ if (typeof message !== "string") return false
25
+ if (!Array.isArray(path)) return false
26
+ return path.every((part) => typeof part === "string")
27
+ }
28
+
29
+ function isInitError(error: unknown): error is InitError {
30
+ return (
31
+ typeof error === "object" &&
32
+ error !== null &&
33
+ "name" in error &&
34
+ "data" in error &&
35
+ typeof (error as InitError).data === "object"
36
+ )
37
+ }
38
+
39
+ function safeJson(value: unknown, circular: string): string {
40
+ const seen = new WeakSet<object>()
41
+ const json = JSON.stringify(
42
+ value,
43
+ (_key, val) => {
44
+ if (typeof val === "bigint") return val.toString()
45
+ if (typeof val === "object" && val) {
46
+ if (seen.has(val)) return circular
47
+ seen.add(val)
48
+ }
49
+ return val
50
+ },
51
+ 2,
52
+ )
53
+ return json ?? String(value)
54
+ }
55
+
56
+ function formatInitError(error: InitError, t: Translator): string {
57
+ const data = error.data
58
+ const json = (value: unknown) => safeJson(value, t("error.page.circular"))
59
+ switch (error.name) {
60
+ case "MCPFailed": {
61
+ const name = typeof data.name === "string" ? data.name : ""
62
+ return t("error.chain.mcpFailed", { name })
63
+ }
64
+ case "ProviderAuthError": {
65
+ const providerID = typeof data.providerID === "string" ? data.providerID : t("common.unknown")
66
+ const message = typeof data.message === "string" ? data.message : json(data.message)
67
+ return t("error.chain.providerAuthFailed", { provider: providerID, message })
68
+ }
69
+ case "APIError": {
70
+ const message = typeof data.message === "string" ? data.message : t("error.chain.apiError")
71
+ const lines: string[] = [message]
72
+
73
+ if (typeof data.statusCode === "number") {
74
+ lines.push(t("error.chain.status", { status: data.statusCode }))
75
+ }
76
+
77
+ if (typeof data.isRetryable === "boolean") {
78
+ lines.push(t("error.chain.retryable", { retryable: data.isRetryable }))
79
+ }
80
+
81
+ if (typeof data.responseBody === "string" && data.responseBody) {
82
+ lines.push(t("error.chain.responseBody", { body: data.responseBody }))
83
+ }
84
+
85
+ return lines.join("\n")
86
+ }
87
+ case "ProviderModelNotFoundError": {
88
+ const { providerID, modelID, suggestions } = data as {
89
+ providerID: string
90
+ modelID: string
91
+ suggestions?: string[]
92
+ }
93
+
94
+ const suggestionsLine =
95
+ Array.isArray(suggestions) && suggestions.length
96
+ ? [t("error.chain.didYouMean", { suggestions: suggestions.join(", ") })]
97
+ : []
98
+
99
+ return [
100
+ t("error.chain.modelNotFound", { provider: providerID, model: modelID }),
101
+ ...suggestionsLine,
102
+ t("error.chain.checkConfig"),
103
+ ].join("\n")
104
+ }
105
+ case "ProviderInitError": {
106
+ const providerID = typeof data.providerID === "string" ? data.providerID : t("common.unknown")
107
+ return t("error.chain.providerInitFailed", { provider: providerID })
108
+ }
109
+ case "ConfigJsonError": {
110
+ const path = typeof data.path === "string" ? data.path : json(data.path)
111
+ const message = typeof data.message === "string" ? data.message : ""
112
+ if (message) return t("error.chain.configJsonInvalidWithMessage", { path, message })
113
+ return t("error.chain.configJsonInvalid", { path })
114
+ }
115
+ case "ConfigDirectoryTypoError": {
116
+ const path = typeof data.path === "string" ? data.path : json(data.path)
117
+ const dir = typeof data.dir === "string" ? data.dir : json(data.dir)
118
+ const suggestion = typeof data.suggestion === "string" ? data.suggestion : json(data.suggestion)
119
+ return t("error.chain.configDirectoryTypo", { dir, path, suggestion })
120
+ }
121
+ case "ConfigFrontmatterError": {
122
+ const path = typeof data.path === "string" ? data.path : json(data.path)
123
+ const message = typeof data.message === "string" ? data.message : json(data.message)
124
+ return t("error.chain.configFrontmatterError", { path, message })
125
+ }
126
+ case "ConfigInvalidError": {
127
+ const issues = Array.isArray(data.issues)
128
+ ? data.issues.filter(isIssue).map((issue) => "↳ " + issue.message + " " + issue.path.join("."))
129
+ : []
130
+ const message = typeof data.message === "string" ? data.message : ""
131
+ const path = typeof data.path === "string" ? data.path : json(data.path)
132
+
133
+ const line = message
134
+ ? t("error.chain.configInvalidWithMessage", { path, message })
135
+ : t("error.chain.configInvalid", { path })
136
+
137
+ return [line, ...issues].join("\n")
138
+ }
139
+ case "UnknownError":
140
+ return typeof data.message === "string" ? data.message : json(data)
141
+ default:
142
+ if (typeof data.message === "string") return data.message
143
+ return json(data)
144
+ }
145
+ }
146
+
147
+ function formatErrorChain(error: unknown, t: Translator, depth = 0, parentMessage?: string): string {
148
+ const json = (value: unknown) => safeJson(value, t("error.page.circular"))
149
+ if (!error) return t("error.chain.unknown")
150
+
151
+ if (isInitError(error)) {
152
+ const message = formatInitError(error, t)
153
+ if (depth > 0 && parentMessage === message) return ""
154
+ const indent = depth > 0 ? `\n${CHAIN_SEPARATOR}${t("error.chain.causedBy")}\n` : ""
155
+ return indent + `${error.name}\n${message}`
156
+ }
157
+
158
+ if (error instanceof Error) {
159
+ const isDuplicate = depth > 0 && parentMessage === error.message
160
+ const parts: string[] = []
161
+ const indent = depth > 0 ? `\n${CHAIN_SEPARATOR}${t("error.chain.causedBy")}\n` : ""
162
+
163
+ const header = `${error.name}${error.message ? `: ${error.message}` : ""}`
164
+ const stack = error.stack?.trim()
165
+
166
+ if (stack) {
167
+ const startsWithHeader = stack.startsWith(header)
168
+
169
+ if (isDuplicate && startsWithHeader) {
170
+ const trace = stack.split("\n").slice(1).join("\n").trim()
171
+ if (trace) {
172
+ parts.push(indent + trace)
173
+ }
174
+ }
175
+
176
+ if (isDuplicate && !startsWithHeader) {
177
+ parts.push(indent + stack)
178
+ }
179
+
180
+ if (!isDuplicate && startsWithHeader) {
181
+ parts.push(indent + stack)
182
+ }
183
+
184
+ if (!isDuplicate && !startsWithHeader) {
185
+ parts.push(indent + `${header}\n${stack}`)
186
+ }
187
+ }
188
+
189
+ if (!stack && !isDuplicate) {
190
+ parts.push(indent + header)
191
+ }
192
+
193
+ if (error.cause) {
194
+ const causeResult = formatErrorChain(error.cause, t, depth + 1, error.message)
195
+ if (causeResult) {
196
+ parts.push(causeResult)
197
+ }
198
+ }
199
+
200
+ return parts.join("\n\n")
201
+ }
202
+
203
+ if (typeof error === "string") {
204
+ if (depth > 0 && parentMessage === error) return ""
205
+ const indent = depth > 0 ? `\n${CHAIN_SEPARATOR}${t("error.chain.causedBy")}\n` : ""
206
+ return indent + error
207
+ }
208
+
209
+ const indent = depth > 0 ? `\n${CHAIN_SEPARATOR}${t("error.chain.causedBy")}\n` : ""
210
+ return indent + json(error)
211
+ }
212
+
213
+ function formatError(error: unknown, t: Translator): string {
214
+ return formatErrorChain(error, t, 0)
215
+ }
216
+
217
+ interface ErrorPageProps {
218
+ error: unknown
219
+ }
220
+
221
+ export const ErrorPage: Component<ErrorPageProps> = (props) => {
222
+ const platform = usePlatform()
223
+ const language = useLanguage()
224
+ const [store, setStore] = createStore({
225
+ checking: false,
226
+ version: undefined as string | undefined,
227
+ actionError: undefined as string | undefined,
228
+ })
229
+
230
+ onMount(() => {
231
+ const win = window as E2EWindow
232
+ if (!win.__opencode_e2e) return
233
+ const detail = formatError(props.error, language.t)
234
+ console.error(`[e2e:error-boundary] ${window.location.pathname}\n${detail}`)
235
+ })
236
+
237
+ async function checkForUpdates() {
238
+ if (!platform.checkUpdate) return
239
+ setStore("checking", true)
240
+ await platform
241
+ .checkUpdate()
242
+ .then((result) => {
243
+ setStore("actionError", undefined)
244
+ if (result.updateAvailable && result.version) setStore("version", result.version)
245
+ })
246
+ .catch((err) => {
247
+ setStore("actionError", formatError(err, language.t))
248
+ })
249
+ .finally(() => {
250
+ setStore("checking", false)
251
+ })
252
+ }
253
+
254
+ async function installUpdate() {
255
+ if (!platform.update || !platform.restart) return
256
+ await platform
257
+ .update()
258
+ .then(() => platform.restart!())
259
+ .then(() => setStore("actionError", undefined))
260
+ .catch((err) => {
261
+ setStore("actionError", formatError(err, language.t))
262
+ })
263
+ }
264
+
265
+ return (
266
+ <div class="relative flex-1 h-screen w-screen min-h-0 flex flex-col items-center justify-center bg-background-base font-sans">
267
+ <div class="w-2/3 max-w-3xl flex flex-col items-center justify-center gap-8">
268
+ <Logo class="w-58.5 opacity-12 shrink-0" />
269
+ <div class="flex flex-col items-center gap-2 text-center">
270
+ <h1 class="text-lg font-medium text-text-strong">{language.t("error.page.title")}</h1>
271
+ <p class="text-sm text-text-weak">{language.t("error.page.description")}</p>
272
+ </div>
273
+ <TextField
274
+ value={formatError(props.error, language.t)}
275
+ readOnly
276
+ copyable
277
+ multiline
278
+ class="max-h-96 w-full font-mono text-xs no-scrollbar"
279
+ label={language.t("error.page.details.label")}
280
+ hideLabel
281
+ />
282
+ <div class="flex items-center gap-3">
283
+ <Button size="large" onClick={platform.restart}>
284
+ {language.t("error.page.action.restart")}
285
+ </Button>
286
+ <Show when={platform.checkUpdate}>
287
+ <Show
288
+ when={store.version}
289
+ fallback={
290
+ <Button size="large" variant="ghost" onClick={checkForUpdates} disabled={store.checking}>
291
+ {store.checking
292
+ ? language.t("error.page.action.checking")
293
+ : language.t("error.page.action.checkUpdates")}
294
+ </Button>
295
+ }
296
+ >
297
+ <Button size="large" onClick={installUpdate}>
298
+ {language.t("error.page.action.updateTo", { version: store.version ?? "" })}
299
+ </Button>
300
+ </Show>
301
+ </Show>
302
+ </div>
303
+ <Show when={store.actionError}>
304
+ {(message) => <p class="text-xs text-text-danger-base text-center max-w-2xl">{message()}</p>}
305
+ </Show>
306
+ <div class="flex flex-col items-center gap-2">
307
+ <div class="flex items-center justify-center gap-1">
308
+ {language.t("error.page.report.prefix")}
309
+ <button
310
+ type="button"
311
+ class="flex items-center text-text-interactive-base gap-1"
312
+ onClick={() => platform.openLink("https://code.reign-labs.com/desktop-feedback")}
313
+ >
314
+ <div>{language.t("error.page.report.discord")}</div>
315
+ <Icon name="discord" class="text-text-interactive-base" />
316
+ </button>
317
+ </div>
318
+ <Show when={platform.version}>
319
+ {(version) => (
320
+ <p class="text-xs text-text-weak">{language.t("error.page.version", { version: version() })}</p>
321
+ )}
322
+ </Show>
323
+ </div>
324
+ </div>
325
+ </div>
326
+ )
327
+ }
@@ -0,0 +1,131 @@
1
+ import { createMemo, For, Match, Switch } from "solid-js"
2
+ import { Button } from "@reign-labs/ui/button"
3
+ import { Logo } from "@reign-labs/ui/logo"
4
+ import { useLayout } from "@/context/layout"
5
+ import { useNavigate } from "@solidjs/router"
6
+ import { base64Encode } from "@reign-labs/util/encode"
7
+ import { Icon } from "@reign-labs/ui/icon"
8
+ import { usePlatform } from "@/context/platform"
9
+ import { DateTime } from "luxon"
10
+ import { useDialog } from "@reign-labs/ui/context/dialog"
11
+ import { DialogSelectDirectory } from "@/components/dialog-select-directory"
12
+ import { DialogSelectServer } from "@/components/dialog-select-server"
13
+ import { useServer } from "@/context/server"
14
+ import { useGlobalSync } from "@/context/global-sync"
15
+ import { useLanguage } from "@/context/language"
16
+
17
+ export default function Home() {
18
+ const sync = useGlobalSync()
19
+ const layout = useLayout()
20
+ const platform = usePlatform()
21
+ const dialog = useDialog()
22
+ const navigate = useNavigate()
23
+ const server = useServer()
24
+ const language = useLanguage()
25
+ const homedir = createMemo(() => sync.data.path.home)
26
+ const recent = createMemo(() => {
27
+ return sync.data.project
28
+ .slice()
29
+ .sort((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
30
+ .slice(0, 5)
31
+ })
32
+
33
+ const serverDotClass = createMemo(() => {
34
+ const healthy = server.healthy()
35
+ if (healthy === true) return "bg-icon-success-base"
36
+ if (healthy === false) return "bg-icon-critical-base"
37
+ return "bg-border-weak-base"
38
+ })
39
+
40
+ function openProject(directory: string) {
41
+ layout.projects.open(directory)
42
+ server.projects.touch(directory)
43
+ navigate(`/${base64Encode(directory)}`)
44
+ }
45
+
46
+ async function chooseProject() {
47
+ function resolve(result: string | string[] | null) {
48
+ if (Array.isArray(result)) {
49
+ for (const directory of result) {
50
+ openProject(directory)
51
+ }
52
+ } else if (result) {
53
+ openProject(result)
54
+ }
55
+ }
56
+
57
+ if (platform.openDirectoryPickerDialog && server.isLocal()) {
58
+ const result = await platform.openDirectoryPickerDialog?.({
59
+ title: language.t("command.project.open"),
60
+ multiple: true,
61
+ })
62
+ resolve(result)
63
+ } else {
64
+ dialog.show(
65
+ () => <DialogSelectDirectory multiple={true} onSelect={resolve} />,
66
+ () => resolve(null),
67
+ )
68
+ }
69
+ }
70
+
71
+ return (
72
+ <div class="mx-auto mt-55 w-full md:w-auto px-4">
73
+ <Logo class="md:w-xl opacity-12" />
74
+ <Button
75
+ size="large"
76
+ variant="ghost"
77
+ class="mt-4 mx-auto text-14-regular text-text-weak"
78
+ onClick={() => dialog.show(() => <DialogSelectServer />)}
79
+ >
80
+ <div
81
+ classList={{
82
+ "size-2 rounded-full": true,
83
+ [serverDotClass()]: true,
84
+ }}
85
+ />
86
+ {server.name}
87
+ </Button>
88
+ <Switch>
89
+ <Match when={sync.data.project.length > 0}>
90
+ <div class="mt-20 w-full flex flex-col gap-4">
91
+ <div class="flex gap-2 items-center justify-between pl-3">
92
+ <div class="text-14-medium text-text-strong">{language.t("home.recentProjects")}</div>
93
+ <Button icon="folder-add-left" size="normal" class="pl-2 pr-3" onClick={chooseProject}>
94
+ {language.t("command.project.open")}
95
+ </Button>
96
+ </div>
97
+ <ul class="flex flex-col gap-2">
98
+ <For each={recent()}>
99
+ {(project) => (
100
+ <Button
101
+ size="large"
102
+ variant="ghost"
103
+ class="text-14-mono text-left justify-between px-3"
104
+ onClick={() => openProject(project.worktree)}
105
+ >
106
+ {project.worktree.replace(homedir(), "~")}
107
+ <div class="text-14-regular text-text-weak">
108
+ {DateTime.fromMillis(project.time.updated ?? project.time.created).toRelative()}
109
+ </div>
110
+ </Button>
111
+ )}
112
+ </For>
113
+ </ul>
114
+ </div>
115
+ </Match>
116
+ <Match when={true}>
117
+ <div class="mt-30 mx-auto flex flex-col items-center gap-3">
118
+ <Icon name="folder-add-left" size="large" />
119
+ <div class="flex flex-col gap-1 items-center justify-center">
120
+ <div class="text-14-medium text-text-strong">{language.t("home.empty.title")}</div>
121
+ <div class="text-12-regular text-text-weak">{language.t("home.empty.description")}</div>
122
+ </div>
123
+ <Button class="px-3 mt-1" onClick={chooseProject}>
124
+ {language.t("command.project.open")}
125
+ </Button>
126
+ </div>
127
+ </Match>
128
+ </Switch>
129
+ </div>
130
+ )
131
+ }
@@ -0,0 +1,50 @@
1
+ export const deepLinkEvent = "reigncode:deep-link"
2
+
3
+ const parseUrl = (input: string) => {
4
+ if (!input.startsWith("reigncode://")) return
5
+ if (typeof URL.canParse === "function" && !URL.canParse(input)) return
6
+ try {
7
+ return new URL(input)
8
+ } catch {
9
+ return
10
+ }
11
+ }
12
+
13
+ export const parseDeepLink = (input: string) => {
14
+ const url = parseUrl(input)
15
+ if (!url) return
16
+ if (url.hostname !== "open-project") return
17
+ const directory = url.searchParams.get("directory")
18
+ if (!directory) return
19
+ return directory
20
+ }
21
+
22
+ export const parseNewSessionDeepLink = (input: string) => {
23
+ const url = parseUrl(input)
24
+ if (!url) return
25
+ if (url.hostname !== "new-session") return
26
+ const directory = url.searchParams.get("directory")
27
+ if (!directory) return
28
+ const prompt = url.searchParams.get("prompt") || undefined
29
+ if (!prompt) return { directory }
30
+ return { directory, prompt }
31
+ }
32
+
33
+ export const collectOpenProjectDeepLinks = (urls: string[]) =>
34
+ urls.map(parseDeepLink).filter((directory): directory is string => !!directory)
35
+
36
+ export const collectNewSessionDeepLinks = (urls: string[]) =>
37
+ urls.map(parseNewSessionDeepLink).filter((link): link is { directory: string; prompt?: string } => !!link)
38
+
39
+ type OpenCodeWindow = Window & {
40
+ __REIGNCODE__?: {
41
+ deepLinks?: string[]
42
+ }
43
+ }
44
+
45
+ export const drainPendingDeepLinks = (target: OpenCodeWindow) => {
46
+ const pending = target.__REIGNCODE__?.deepLinks ?? []
47
+ if (pending.length === 0) return []
48
+ if (target.__REIGNCODE__) target.__REIGNCODE__.deepLinks = []
49
+ return pending
50
+ }