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,62 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import {
3
+ disposeIfDisposable,
4
+ getHoveredLinkText,
5
+ getSpeechRecognitionCtor,
6
+ hasSetOption,
7
+ isDisposable,
8
+ setOptionIfSupported,
9
+ } from "./runtime-adapters"
10
+
11
+ describe("runtime adapters", () => {
12
+ test("detects and disposes disposable values", () => {
13
+ let count = 0
14
+ const value = {
15
+ dispose: () => {
16
+ count += 1
17
+ },
18
+ }
19
+ expect(isDisposable(value)).toBe(true)
20
+ disposeIfDisposable(value)
21
+ expect(count).toBe(1)
22
+ })
23
+
24
+ test("ignores non-disposable values", () => {
25
+ expect(isDisposable({ dispose: "nope" })).toBe(false)
26
+ expect(() => disposeIfDisposable({ dispose: "nope" })).not.toThrow()
27
+ })
28
+
29
+ test("sets options only when setter exists", () => {
30
+ const calls: Array<[string, unknown]> = []
31
+ const value = {
32
+ setOption: (key: string, next: unknown) => {
33
+ calls.push([key, next])
34
+ },
35
+ }
36
+ expect(hasSetOption(value)).toBe(true)
37
+ setOptionIfSupported(value, "fontFamily", "Berkeley Mono")
38
+ expect(calls).toEqual([["fontFamily", "Berkeley Mono"]])
39
+ expect(() => setOptionIfSupported({}, "fontFamily", "Berkeley Mono")).not.toThrow()
40
+ })
41
+
42
+ test("reads hovered link text safely", () => {
43
+ expect(getHoveredLinkText({ currentHoveredLink: { text: "https://example.com" } })).toBe("https://example.com")
44
+ expect(getHoveredLinkText({ currentHoveredLink: { text: 1 } })).toBeUndefined()
45
+ expect(getHoveredLinkText(null)).toBeUndefined()
46
+ })
47
+
48
+ test("resolves speech recognition constructor with webkit precedence", () => {
49
+ class SpeechCtor {}
50
+ class WebkitCtor {}
51
+ const ctor = getSpeechRecognitionCtor({
52
+ SpeechRecognition: SpeechCtor,
53
+ webkitSpeechRecognition: WebkitCtor,
54
+ })
55
+ expect(ctor).toBe(WebkitCtor)
56
+ })
57
+
58
+ test("returns undefined when no valid speech constructor exists", () => {
59
+ expect(getSpeechRecognitionCtor({ SpeechRecognition: "nope" })).toBeUndefined()
60
+ expect(getSpeechRecognitionCtor(undefined)).toBeUndefined()
61
+ })
62
+ })
@@ -0,0 +1,39 @@
1
+ type RecordValue = Record<string, unknown>
2
+
3
+ const isRecord = (value: unknown): value is RecordValue => {
4
+ return typeof value === "object" && value !== null
5
+ }
6
+
7
+ export const isDisposable = (value: unknown): value is { dispose: () => void } => {
8
+ return isRecord(value) && typeof value.dispose === "function"
9
+ }
10
+
11
+ export const disposeIfDisposable = (value: unknown) => {
12
+ if (!isDisposable(value)) return
13
+ value.dispose()
14
+ }
15
+
16
+ export const hasSetOption = (value: unknown): value is { setOption: (key: string, next: unknown) => void } => {
17
+ return isRecord(value) && typeof value.setOption === "function"
18
+ }
19
+
20
+ export const setOptionIfSupported = (value: unknown, key: string, next: unknown) => {
21
+ if (!hasSetOption(value)) return
22
+ value.setOption(key, next)
23
+ }
24
+
25
+ export const getHoveredLinkText = (value: unknown) => {
26
+ if (!isRecord(value)) return
27
+ const link = value.currentHoveredLink
28
+ if (!isRecord(link)) return
29
+ if (typeof link.text !== "string") return
30
+ return link.text
31
+ }
32
+
33
+ export const getSpeechRecognitionCtor = <T>(value: unknown): (new () => T) | undefined => {
34
+ if (!isRecord(value)) return
35
+ const ctor =
36
+ typeof value.webkitSpeechRecognition === "function" ? value.webkitSpeechRecognition : value.SpeechRecognition
37
+ if (typeof ctor !== "function") return
38
+ return ctor as new () => T
39
+ }
@@ -0,0 +1,6 @@
1
+ export function same<T>(a: readonly T[] | undefined, b: readonly T[] | undefined) {
2
+ if (a === b) return true
3
+ if (!a || !b) return false
4
+ if (a.length !== b.length) return false
5
+ return a.every((x, i) => x === b[i])
6
+ }
@@ -0,0 +1,69 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { createScopedCache } from "./scoped-cache"
3
+
4
+ describe("createScopedCache", () => {
5
+ test("evicts least-recently-used entry when max is reached", () => {
6
+ const disposed: string[] = []
7
+ const cache = createScopedCache((key) => ({ key }), {
8
+ maxEntries: 2,
9
+ dispose: (value) => disposed.push(value.key),
10
+ })
11
+
12
+ const a = cache.get("a")
13
+ const b = cache.get("b")
14
+ expect(a.key).toBe("a")
15
+ expect(b.key).toBe("b")
16
+
17
+ cache.get("a")
18
+ const c = cache.get("c")
19
+
20
+ expect(c.key).toBe("c")
21
+ expect(cache.peek("a")?.key).toBe("a")
22
+ expect(cache.peek("b")).toBeUndefined()
23
+ expect(cache.peek("c")?.key).toBe("c")
24
+ expect(disposed).toEqual(["b"])
25
+ })
26
+
27
+ test("disposes entries on delete and clear", () => {
28
+ const disposed: string[] = []
29
+ const cache = createScopedCache((key) => ({ key }), {
30
+ dispose: (value) => disposed.push(value.key),
31
+ })
32
+
33
+ cache.get("a")
34
+ cache.get("b")
35
+
36
+ const removed = cache.delete("a")
37
+ expect(removed?.key).toBe("a")
38
+ expect(cache.peek("a")).toBeUndefined()
39
+
40
+ cache.clear()
41
+ expect(cache.peek("b")).toBeUndefined()
42
+ expect(disposed).toEqual(["a", "b"])
43
+ })
44
+
45
+ test("expires stale entries with ttl and recreates on get", () => {
46
+ let clock = 0
47
+ let count = 0
48
+ const disposed: string[] = []
49
+ const cache = createScopedCache((key) => ({ key, count: ++count }), {
50
+ ttlMs: 10,
51
+ now: () => clock,
52
+ dispose: (value) => disposed.push(`${value.key}:${value.count}`),
53
+ })
54
+
55
+ const first = cache.get("a")
56
+ expect(first.count).toBe(1)
57
+
58
+ clock = 9
59
+ expect(cache.peek("a")?.count).toBe(1)
60
+
61
+ clock = 11
62
+ expect(cache.peek("a")).toBeUndefined()
63
+ expect(disposed).toEqual(["a:1"])
64
+
65
+ const second = cache.get("a")
66
+ expect(second.count).toBe(2)
67
+ expect(disposed).toEqual(["a:1"])
68
+ })
69
+ })
@@ -0,0 +1,104 @@
1
+ type ScopedCacheOptions<T> = {
2
+ maxEntries?: number
3
+ ttlMs?: number
4
+ dispose?: (value: T, key: string) => void
5
+ now?: () => number
6
+ }
7
+
8
+ type Entry<T> = {
9
+ value: T
10
+ touchedAt: number
11
+ }
12
+
13
+ export function createScopedCache<T>(createValue: (key: string) => T, options: ScopedCacheOptions<T> = {}) {
14
+ const store = new Map<string, Entry<T>>()
15
+ const now = options.now ?? Date.now
16
+
17
+ const dispose = (key: string, entry: Entry<T>) => {
18
+ options.dispose?.(entry.value, key)
19
+ }
20
+
21
+ const expired = (entry: Entry<T>) => {
22
+ if (options.ttlMs === undefined) return false
23
+ return now() - entry.touchedAt >= options.ttlMs
24
+ }
25
+
26
+ const sweep = () => {
27
+ if (options.ttlMs === undefined) return
28
+ for (const [key, entry] of store) {
29
+ if (!expired(entry)) continue
30
+ store.delete(key)
31
+ dispose(key, entry)
32
+ }
33
+ }
34
+
35
+ const touch = (key: string, entry: Entry<T>) => {
36
+ entry.touchedAt = now()
37
+ store.delete(key)
38
+ store.set(key, entry)
39
+ }
40
+
41
+ const prune = () => {
42
+ if (options.maxEntries === undefined) return
43
+ while (store.size > options.maxEntries) {
44
+ const key = store.keys().next().value
45
+ if (!key) return
46
+ const entry = store.get(key)
47
+ store.delete(key)
48
+ if (!entry) continue
49
+ dispose(key, entry)
50
+ }
51
+ }
52
+
53
+ const remove = (key: string) => {
54
+ const entry = store.get(key)
55
+ if (!entry) return
56
+ store.delete(key)
57
+ dispose(key, entry)
58
+ return entry.value
59
+ }
60
+
61
+ const peek = (key: string) => {
62
+ sweep()
63
+ const entry = store.get(key)
64
+ if (!entry) return
65
+ if (!expired(entry)) return entry.value
66
+ store.delete(key)
67
+ dispose(key, entry)
68
+ }
69
+
70
+ const get = (key: string) => {
71
+ sweep()
72
+ const entry = store.get(key)
73
+ if (entry && !expired(entry)) {
74
+ touch(key, entry)
75
+ return entry.value
76
+ }
77
+ if (entry) {
78
+ store.delete(key)
79
+ dispose(key, entry)
80
+ }
81
+
82
+ const created = {
83
+ value: createValue(key),
84
+ touchedAt: now(),
85
+ }
86
+ store.set(key, created)
87
+ prune()
88
+ return created.value
89
+ }
90
+
91
+ const clear = () => {
92
+ for (const [key, entry] of store) {
93
+ dispose(key, entry)
94
+ }
95
+ store.clear()
96
+ }
97
+
98
+ return {
99
+ get,
100
+ peek,
101
+ delete: remove,
102
+ clear,
103
+ }
104
+ }
@@ -0,0 +1,131 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import type { ConfigInvalidError, ProviderModelNotFoundError } from "./server-errors"
3
+ import { formatServerError, parseReadableConfigInvalidError } from "./server-errors"
4
+
5
+ function fill(text: string, vars?: Record<string, string | number>) {
6
+ if (!vars) return text
7
+ return text.replace(/{{\s*(\w+)\s*}}/g, (_, key: string) => {
8
+ const value = vars[key]
9
+ if (value === undefined) return ""
10
+ return String(value)
11
+ })
12
+ }
13
+
14
+ function useLanguageMock() {
15
+ const dict: Record<string, string> = {
16
+ "error.chain.unknown": "Erro desconhecido",
17
+ "error.chain.configInvalid": "Arquivo de config em {{path}} invalido",
18
+ "error.chain.configInvalidWithMessage": "Arquivo de config em {{path}} invalido: {{message}}",
19
+ "error.chain.modelNotFound": "Modelo nao encontrado: {{provider}}/{{model}}",
20
+ "error.chain.didYouMean": "Voce quis dizer: {{suggestions}}",
21
+ "error.chain.checkConfig": "Revise provider/model no config",
22
+ }
23
+ return {
24
+ t(key: string, vars?: Record<string, string | number>) {
25
+ const text = dict[key]
26
+ if (!text) return key
27
+ return fill(text, vars)
28
+ },
29
+ }
30
+ }
31
+
32
+ const language = useLanguageMock()
33
+
34
+ describe("parseReadableConfigInvalidError", () => {
35
+ test("formats issues with file path", () => {
36
+ const error = {
37
+ name: "ConfigInvalidError",
38
+ data: {
39
+ path: "opencode.config.ts",
40
+ issues: [
41
+ { path: ["settings", "host"], message: "Required" },
42
+ { path: ["mode"], message: "Invalid" },
43
+ ],
44
+ },
45
+ } satisfies ConfigInvalidError
46
+
47
+ const result = parseReadableConfigInvalidError(error, language.t)
48
+
49
+ expect(result).toBe(
50
+ ["Arquivo de config em opencode.config.ts invalido: settings.host: Required", "mode: Invalid"].join("\n"),
51
+ )
52
+ })
53
+
54
+ test("uses trimmed message when issues are missing", () => {
55
+ const error = {
56
+ name: "ConfigInvalidError",
57
+ data: {
58
+ path: "config",
59
+ message: " Bad value ",
60
+ },
61
+ } satisfies ConfigInvalidError
62
+
63
+ const result = parseReadableConfigInvalidError(error, language.t)
64
+
65
+ expect(result).toBe("Arquivo de config em config invalido: Bad value")
66
+ })
67
+ })
68
+
69
+ describe("formatServerError", () => {
70
+ test("formats config invalid errors", () => {
71
+ const error = {
72
+ name: "ConfigInvalidError",
73
+ data: {
74
+ message: "Missing host",
75
+ },
76
+ } satisfies ConfigInvalidError
77
+
78
+ const result = formatServerError(error, language.t)
79
+
80
+ expect(result).toBe("Arquivo de config em config invalido: Missing host")
81
+ })
82
+
83
+ test("returns error messages", () => {
84
+ expect(formatServerError(new Error("Request failed with status 503"), language.t)).toBe(
85
+ "Request failed with status 503",
86
+ )
87
+ })
88
+
89
+ test("returns provided string errors", () => {
90
+ expect(formatServerError("Failed to connect to server", language.t)).toBe("Failed to connect to server")
91
+ })
92
+
93
+ test("uses translated unknown fallback", () => {
94
+ expect(formatServerError(0, language.t)).toBe("Erro desconhecido")
95
+ })
96
+
97
+ test("falls back for unknown error objects and names", () => {
98
+ expect(formatServerError({ name: "ServerTimeoutError", data: { seconds: 30 } }, language.t)).toBe(
99
+ "Erro desconhecido",
100
+ )
101
+ })
102
+
103
+ test("formats provider model errors using provider/model", () => {
104
+ const error = {
105
+ name: "ProviderModelNotFoundError",
106
+ data: {
107
+ providerID: "openai",
108
+ modelID: "gpt-4.1",
109
+ },
110
+ } satisfies ProviderModelNotFoundError
111
+
112
+ expect(formatServerError(error, language.t)).toBe(
113
+ ["Modelo nao encontrado: openai/gpt-4.1", "Revise provider/model no config"].join("\n"),
114
+ )
115
+ })
116
+
117
+ test("formats provider model suggestions", () => {
118
+ const error = {
119
+ name: "ProviderModelNotFoundError",
120
+ data: {
121
+ providerID: "x",
122
+ modelID: "y",
123
+ suggestions: ["x/y2", "x/y3"],
124
+ },
125
+ } satisfies ProviderModelNotFoundError
126
+
127
+ expect(formatServerError(error, language.t)).toBe(
128
+ ["Modelo nao encontrado: x/y", "Voce quis dizer: x/y2, x/y3", "Revise provider/model no config"].join("\n"),
129
+ )
130
+ })
131
+ })
@@ -0,0 +1,80 @@
1
+ export type ConfigInvalidError = {
2
+ name: "ConfigInvalidError"
3
+ data: {
4
+ path?: string
5
+ message?: string
6
+ issues?: Array<{ message: string; path: string[] }>
7
+ }
8
+ }
9
+
10
+ export type ProviderModelNotFoundError = {
11
+ name: "ProviderModelNotFoundError"
12
+ data: {
13
+ providerID: string
14
+ modelID: string
15
+ suggestions?: string[]
16
+ }
17
+ }
18
+
19
+ type Translator = (key: string, vars?: Record<string, string | number>) => string
20
+
21
+ function tr(translator: Translator | undefined, key: string, text: string, vars?: Record<string, string | number>) {
22
+ if (!translator) return text
23
+ const out = translator(key, vars)
24
+ if (!out || out === key) return text
25
+ return out
26
+ }
27
+
28
+ export function formatServerError(error: unknown, translate?: Translator, fallback?: string) {
29
+ if (isConfigInvalidErrorLike(error)) return parseReadableConfigInvalidError(error, translate)
30
+ if (isProviderModelNotFoundErrorLike(error)) return parseReadableProviderModelNotFoundError(error, translate)
31
+ if (error instanceof Error && error.message) return error.message
32
+ if (typeof error === "string" && error) return error
33
+ if (fallback) return fallback
34
+ return tr(translate, "error.chain.unknown", "Unknown error")
35
+ }
36
+
37
+ function isConfigInvalidErrorLike(error: unknown): error is ConfigInvalidError {
38
+ if (typeof error !== "object" || error === null) return false
39
+ const o = error as Record<string, unknown>
40
+ return o.name === "ConfigInvalidError" && typeof o.data === "object" && o.data !== null
41
+ }
42
+
43
+ function isProviderModelNotFoundErrorLike(error: unknown): error is ProviderModelNotFoundError {
44
+ if (typeof error !== "object" || error === null) return false
45
+ const o = error as Record<string, unknown>
46
+ return o.name === "ProviderModelNotFoundError" && typeof o.data === "object" && o.data !== null
47
+ }
48
+
49
+ export function parseReadableConfigInvalidError(errorInput: ConfigInvalidError, translator?: Translator) {
50
+ const file = errorInput.data.path && errorInput.data.path !== "config" ? errorInput.data.path : "config"
51
+ const detail = errorInput.data.message?.trim() ?? ""
52
+ const issues = (errorInput.data.issues ?? [])
53
+ .map((issue) => {
54
+ const msg = issue.message.trim()
55
+ if (!issue.path.length) return msg
56
+ return `${issue.path.join(".")}: ${msg}`
57
+ })
58
+ .filter(Boolean)
59
+ const msg = issues.length ? issues.join("\n") : detail
60
+ if (!msg) return tr(translator, "error.chain.configInvalid", `Config file at ${file} is invalid`, { path: file })
61
+ return tr(translator, "error.chain.configInvalidWithMessage", `Config file at ${file} is invalid: ${msg}`, {
62
+ path: file,
63
+ message: msg,
64
+ })
65
+ }
66
+
67
+ function parseReadableProviderModelNotFoundError(errorInput: ProviderModelNotFoundError, translator?: Translator) {
68
+ const p = errorInput.data.providerID.trim()
69
+ const m = errorInput.data.modelID.trim()
70
+ const list = (errorInput.data.suggestions ?? []).map((v) => v.trim()).filter(Boolean)
71
+ const body = tr(translator, "error.chain.modelNotFound", `Model not found: ${p}/${m}`, { provider: p, model: m })
72
+ const tail = tr(translator, "error.chain.checkConfig", "Check your config (reigncode.json) provider/model names")
73
+ if (list.length) {
74
+ const suggestions = list.slice(0, 5).join(", ")
75
+ return [body, tr(translator, "error.chain.didYouMean", `Did you mean: ${suggestions}`, { suggestions }), tail].join(
76
+ "\n",
77
+ )
78
+ }
79
+ return [body, tail].join("\n")
80
+ }
@@ -0,0 +1,123 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import type { ServerConnection } from "@/context/server"
3
+ import { checkServerHealth } from "./server-health"
4
+
5
+ const server: ServerConnection.HttpBase = {
6
+ url: "http://localhost:4096",
7
+ }
8
+
9
+ function abortFromInput(input: RequestInfo | URL, init?: RequestInit) {
10
+ if (init?.signal) return init.signal
11
+ if (input instanceof Request) return input.signal
12
+ return undefined
13
+ }
14
+
15
+ describe("checkServerHealth", () => {
16
+ test("returns healthy response with version", async () => {
17
+ const fetch = (async () =>
18
+ new Response(JSON.stringify({ healthy: true, version: "1.2.3" }), {
19
+ status: 200,
20
+ headers: { "content-type": "application/json" },
21
+ })) as unknown as typeof globalThis.fetch
22
+
23
+ const result = await checkServerHealth(server, fetch)
24
+
25
+ expect(result).toEqual({ healthy: true, version: "1.2.3" })
26
+ })
27
+
28
+ test("returns unhealthy when request fails", async () => {
29
+ const fetch = (async () => {
30
+ throw new Error("network")
31
+ }) as unknown as typeof globalThis.fetch
32
+
33
+ const result = await checkServerHealth(server, fetch)
34
+
35
+ expect(result).toEqual({ healthy: false })
36
+ })
37
+
38
+ test("uses timeout fallback when AbortSignal.timeout is unavailable", async () => {
39
+ const timeout = Object.getOwnPropertyDescriptor(AbortSignal, "timeout")
40
+ Object.defineProperty(AbortSignal, "timeout", {
41
+ configurable: true,
42
+ value: undefined,
43
+ })
44
+
45
+ let aborted = false
46
+ const fetch = ((input: RequestInfo | URL, init?: RequestInit) =>
47
+ new Promise<Response>((_resolve, reject) => {
48
+ const signal = abortFromInput(input, init)
49
+ signal?.addEventListener(
50
+ "abort",
51
+ () => {
52
+ aborted = true
53
+ reject(new DOMException("Aborted", "AbortError"))
54
+ },
55
+ { once: true },
56
+ )
57
+ })) as unknown as typeof globalThis.fetch
58
+
59
+ const result = await checkServerHealth(server, fetch, {
60
+ timeoutMs: 10,
61
+ }).finally(() => {
62
+ if (timeout) Object.defineProperty(AbortSignal, "timeout", timeout)
63
+ if (!timeout) Reflect.deleteProperty(AbortSignal, "timeout")
64
+ })
65
+
66
+ expect(aborted).toBe(true)
67
+ expect(result).toEqual({ healthy: false })
68
+ })
69
+
70
+ test("uses provided abort signal", async () => {
71
+ let signal: AbortSignal | undefined
72
+ const fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
73
+ signal = abortFromInput(input, init)
74
+ return new Response(JSON.stringify({ healthy: true, version: "1.2.3" }), {
75
+ status: 200,
76
+ headers: { "content-type": "application/json" },
77
+ })
78
+ }) as unknown as typeof globalThis.fetch
79
+
80
+ const abort = new AbortController()
81
+ await checkServerHealth(server, fetch, {
82
+ signal: abort.signal,
83
+ })
84
+
85
+ expect(signal).toBe(abort.signal)
86
+ })
87
+
88
+ test("retries transient failures and eventually succeeds", async () => {
89
+ let count = 0
90
+ const fetch = (async () => {
91
+ count += 1
92
+ if (count < 3) throw new TypeError("network")
93
+ return new Response(JSON.stringify({ healthy: true, version: "1.2.3" }), {
94
+ status: 200,
95
+ headers: { "content-type": "application/json" },
96
+ })
97
+ }) as unknown as typeof globalThis.fetch
98
+
99
+ const result = await checkServerHealth(server, fetch, {
100
+ retryCount: 2,
101
+ retryDelayMs: 1,
102
+ })
103
+
104
+ expect(count).toBe(3)
105
+ expect(result).toEqual({ healthy: true, version: "1.2.3" })
106
+ })
107
+
108
+ test("returns unhealthy when retries are exhausted", async () => {
109
+ let count = 0
110
+ const fetch = (async () => {
111
+ count += 1
112
+ throw new TypeError("network")
113
+ }) as unknown as typeof globalThis.fetch
114
+
115
+ const result = await checkServerHealth(server, fetch, {
116
+ retryCount: 2,
117
+ retryDelayMs: 1,
118
+ })
119
+
120
+ expect(count).toBe(3)
121
+ expect(result).toEqual({ healthy: false })
122
+ })
123
+ })