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,65 @@
1
+ import { afterEach, describe, expect, test } from "bun:test"
2
+ import {
3
+ evictContentLru,
4
+ getFileContentBytesTotal,
5
+ getFileContentEntryCount,
6
+ removeFileContentBytes,
7
+ resetFileContentLru,
8
+ setFileContentBytes,
9
+ touchFileContent,
10
+ } from "./file/content-cache"
11
+
12
+ describe("file content eviction accounting", () => {
13
+ afterEach(() => {
14
+ resetFileContentLru()
15
+ })
16
+
17
+ test("updates byte totals incrementally for set, overwrite, remove, and reset", () => {
18
+ setFileContentBytes("a", 10)
19
+ setFileContentBytes("b", 15)
20
+ expect(getFileContentBytesTotal()).toBe(25)
21
+ expect(getFileContentEntryCount()).toBe(2)
22
+
23
+ setFileContentBytes("a", 5)
24
+ expect(getFileContentBytesTotal()).toBe(20)
25
+ expect(getFileContentEntryCount()).toBe(2)
26
+
27
+ touchFileContent("a")
28
+ expect(getFileContentBytesTotal()).toBe(20)
29
+
30
+ removeFileContentBytes("b")
31
+ expect(getFileContentBytesTotal()).toBe(5)
32
+ expect(getFileContentEntryCount()).toBe(1)
33
+
34
+ resetFileContentLru()
35
+ expect(getFileContentBytesTotal()).toBe(0)
36
+ expect(getFileContentEntryCount()).toBe(0)
37
+ })
38
+
39
+ test("evicts by entry cap using LRU order", () => {
40
+ for (const i of Array.from({ length: 41 }, (_, n) => n)) {
41
+ setFileContentBytes(`f-${i}`, 1)
42
+ }
43
+
44
+ const evicted: string[] = []
45
+ evictContentLru(undefined, (path) => evicted.push(path))
46
+
47
+ expect(evicted).toEqual(["f-0"])
48
+ expect(getFileContentEntryCount()).toBe(40)
49
+ expect(getFileContentBytesTotal()).toBe(40)
50
+ })
51
+
52
+ test("evicts by byte cap while preserving protected entries", () => {
53
+ const chunk = 8 * 1024 * 1024
54
+ setFileContentBytes("a", chunk)
55
+ setFileContentBytes("b", chunk)
56
+ setFileContentBytes("c", chunk)
57
+
58
+ const evicted: string[] = []
59
+ evictContentLru(new Set(["a"]), (path) => evicted.push(path))
60
+
61
+ expect(evicted).toEqual(["b"])
62
+ expect(getFileContentEntryCount()).toBe(2)
63
+ expect(getFileContentBytesTotal()).toBe(chunk * 2)
64
+ })
65
+ })
@@ -0,0 +1,280 @@
1
+ import { batch, createEffect, createMemo, onCleanup } from "solid-js"
2
+ import { createStore, produce, reconcile } from "solid-js/store"
3
+ import { createSimpleContext } from "@reign-labs/ui/context"
4
+ import { showToast } from "@reign-labs/ui/toast"
5
+ import { useParams } from "@solidjs/router"
6
+ import { getFilename } from "@reign-labs/util/path"
7
+ import { useSDK } from "./sdk"
8
+ import { useSync } from "./sync"
9
+ import { useLanguage } from "@/context/language"
10
+ import { useLayout } from "@/context/layout"
11
+ import { createPathHelpers } from "./file/path"
12
+ import {
13
+ approxBytes,
14
+ evictContentLru,
15
+ getFileContentBytesTotal,
16
+ getFileContentEntryCount,
17
+ hasFileContent,
18
+ removeFileContentBytes,
19
+ resetFileContentLru,
20
+ setFileContentBytes,
21
+ touchFileContent,
22
+ } from "./file/content-cache"
23
+ import { createFileViewCache } from "./file/view-cache"
24
+ import { createFileTreeStore } from "./file/tree-store"
25
+ import { invalidateFromWatcher } from "./file/watcher"
26
+ import {
27
+ selectionFromLines,
28
+ type FileState,
29
+ type FileSelection,
30
+ type FileViewState,
31
+ type SelectedLineRange,
32
+ } from "./file/types"
33
+
34
+ export type { FileSelection, SelectedLineRange, FileViewState, FileState }
35
+ export { selectionFromLines }
36
+ export {
37
+ evictContentLru,
38
+ getFileContentBytesTotal,
39
+ getFileContentEntryCount,
40
+ removeFileContentBytes,
41
+ resetFileContentLru,
42
+ setFileContentBytes,
43
+ touchFileContent,
44
+ }
45
+
46
+ function errorMessage(error: unknown, fallback: string) {
47
+ if (error instanceof Error && error.message) return error.message
48
+ if (typeof error === "string" && error) return error
49
+ return fallback
50
+ }
51
+
52
+ export const { use: useFile, provider: FileProvider } = createSimpleContext({
53
+ name: "File",
54
+ gate: false,
55
+ init: () => {
56
+ const sdk = useSDK()
57
+ useSync()
58
+ const params = useParams()
59
+ const language = useLanguage()
60
+ const layout = useLayout()
61
+
62
+ const scope = createMemo(() => sdk.directory)
63
+ const path = createPathHelpers(scope)
64
+ const tabs = layout.tabs(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
65
+
66
+ const inflight = new Map<string, Promise<void>>()
67
+ const [store, setStore] = createStore<{
68
+ file: Record<string, FileState>
69
+ }>({
70
+ file: {},
71
+ })
72
+
73
+ const tree = createFileTreeStore({
74
+ scope,
75
+ normalizeDir: path.normalizeDir,
76
+ list: (dir) => sdk.client.file.list({ path: dir }).then((x) => x.data ?? []),
77
+ onError: (message) => {
78
+ showToast({
79
+ variant: "error",
80
+ title: language.t("toast.file.listFailed.title"),
81
+ description: message,
82
+ })
83
+ },
84
+ })
85
+
86
+ const evictContent = (keep?: Set<string>) => {
87
+ evictContentLru(keep, (target) => {
88
+ if (!store.file[target]) return
89
+ setStore(
90
+ "file",
91
+ target,
92
+ produce((draft) => {
93
+ draft.content = undefined
94
+ draft.loaded = false
95
+ }),
96
+ )
97
+ })
98
+ }
99
+
100
+ createEffect(() => {
101
+ scope()
102
+ inflight.clear()
103
+ resetFileContentLru()
104
+ batch(() => {
105
+ setStore("file", reconcile({}))
106
+ tree.reset()
107
+ })
108
+ })
109
+
110
+ const viewCache = createFileViewCache()
111
+ const view = createMemo(() => viewCache.load(scope(), params.id))
112
+
113
+ const ensure = (file: string) => {
114
+ if (!file) return
115
+ if (store.file[file]) return
116
+ setStore("file", file, { path: file, name: getFilename(file) })
117
+ }
118
+
119
+ const setLoading = (file: string) => {
120
+ setStore(
121
+ "file",
122
+ file,
123
+ produce((draft) => {
124
+ draft.loading = true
125
+ draft.error = undefined
126
+ }),
127
+ )
128
+ }
129
+
130
+ const setLoaded = (file: string, content: FileState["content"]) => {
131
+ setStore(
132
+ "file",
133
+ file,
134
+ produce((draft) => {
135
+ draft.loaded = true
136
+ draft.loading = false
137
+ draft.content = content
138
+ }),
139
+ )
140
+ }
141
+
142
+ const setLoadError = (file: string, message: string) => {
143
+ setStore(
144
+ "file",
145
+ file,
146
+ produce((draft) => {
147
+ draft.loading = false
148
+ draft.error = message
149
+ }),
150
+ )
151
+ showToast({
152
+ variant: "error",
153
+ title: language.t("toast.file.loadFailed.title"),
154
+ description: message,
155
+ })
156
+ }
157
+
158
+ const load = (input: string, options?: { force?: boolean }) => {
159
+ const file = path.normalize(input)
160
+ if (!file) return Promise.resolve()
161
+
162
+ const directory = scope()
163
+ const key = `${directory}\n${file}`
164
+ ensure(file)
165
+
166
+ const current = store.file[file]
167
+ if (!options?.force && current?.loaded) return Promise.resolve()
168
+
169
+ const pending = inflight.get(key)
170
+ if (pending) return pending
171
+
172
+ setLoading(file)
173
+
174
+ const promise = sdk.client.file
175
+ .read({ path: file })
176
+ .then((x) => {
177
+ if (scope() !== directory) return
178
+ const content = x.data
179
+ setLoaded(file, content)
180
+
181
+ if (!content) return
182
+ touchFileContent(file, approxBytes(content))
183
+ evictContent(new Set([file]))
184
+ })
185
+ .catch((e) => {
186
+ if (scope() !== directory) return
187
+ setLoadError(file, errorMessage(e, language.t("error.chain.unknown")))
188
+ })
189
+ .finally(() => {
190
+ inflight.delete(key)
191
+ })
192
+
193
+ inflight.set(key, promise)
194
+ return promise
195
+ }
196
+
197
+ const search = (query: string, dirs: "true" | "false") =>
198
+ sdk.client.find.files({ query, dirs }).then(
199
+ (x) => (x.data ?? []).map(path.normalize),
200
+ () => [],
201
+ )
202
+
203
+ const stop = sdk.event.listen((e) => {
204
+ invalidateFromWatcher(e.details, {
205
+ normalize: path.normalize,
206
+ hasFile: (file) => Boolean(store.file[file]),
207
+ isOpen: (file) => tabs.all().some((tab) => path.pathFromTab(tab) === file),
208
+ loadFile: (file) => {
209
+ void load(file, { force: true })
210
+ },
211
+ node: tree.node,
212
+ isDirLoaded: tree.isLoaded,
213
+ refreshDir: (dir) => {
214
+ void tree.listDir(dir, { force: true })
215
+ },
216
+ })
217
+ })
218
+
219
+ const get = (input: string) => {
220
+ const file = path.normalize(input)
221
+ const state = store.file[file]
222
+ const content = state?.content
223
+ if (!content) return state
224
+ if (hasFileContent(file)) {
225
+ touchFileContent(file)
226
+ return state
227
+ }
228
+ touchFileContent(file, approxBytes(content))
229
+ return state
230
+ }
231
+
232
+ function withPath(input: string, action: (file: string) => unknown) {
233
+ return action(path.normalize(input))
234
+ }
235
+ const scrollTop = (input: string) => withPath(input, (file) => view().scrollTop(file))
236
+ const scrollLeft = (input: string) => withPath(input, (file) => view().scrollLeft(file))
237
+ const selectedLines = (input: string) => withPath(input, (file) => view().selectedLines(file))
238
+ const setScrollTop = (input: string, top: number) => withPath(input, (file) => view().setScrollTop(file, top))
239
+ const setScrollLeft = (input: string, left: number) => withPath(input, (file) => view().setScrollLeft(file, left))
240
+ const setSelectedLines = (input: string, range: SelectedLineRange | null) =>
241
+ withPath(input, (file) => view().setSelectedLines(file, range))
242
+
243
+ onCleanup(() => {
244
+ stop()
245
+ viewCache.clear()
246
+ })
247
+
248
+ return {
249
+ ready: () => view().ready(),
250
+ normalize: path.normalize,
251
+ tab: path.tab,
252
+ pathFromTab: path.pathFromTab,
253
+ tree: {
254
+ list: tree.listDir,
255
+ refresh: (input: string) => tree.listDir(input, { force: true }),
256
+ state: tree.dirState,
257
+ children: tree.children,
258
+ expand: tree.expandDir,
259
+ collapse: tree.collapseDir,
260
+ toggle(input: string) {
261
+ if (tree.dirState(input)?.expanded) {
262
+ tree.collapseDir(input)
263
+ return
264
+ }
265
+ tree.expandDir(input)
266
+ },
267
+ },
268
+ get,
269
+ load,
270
+ scrollTop,
271
+ scrollLeft,
272
+ setScrollTop,
273
+ setScrollLeft,
274
+ selectedLines,
275
+ setSelectedLines,
276
+ searchFiles: (query: string) => search(query, "false"),
277
+ searchFilesAndDirectories: (query: string) => search(query, "true"),
278
+ }
279
+ },
280
+ })
@@ -0,0 +1,232 @@
1
+ import type { Event } from "@reign-labs/sdk/v2/client"
2
+ import { createSimpleContext } from "@reign-labs/ui/context"
3
+ import { createGlobalEmitter } from "@solid-primitives/event-bus"
4
+ import { batch, onCleanup } from "solid-js"
5
+ import z from "zod"
6
+ import { createSdkForServer } from "@/utils/server"
7
+ import { useLanguage } from "./language"
8
+ import { usePlatform } from "./platform"
9
+ import { useServer } from "./server"
10
+
11
+ const abortError = z.object({
12
+ name: z.literal("AbortError"),
13
+ })
14
+
15
+ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleContext({
16
+ name: "GlobalSDK",
17
+ init: () => {
18
+ const language = useLanguage()
19
+ const server = useServer()
20
+ const platform = usePlatform()
21
+ const abort = new AbortController()
22
+
23
+ const eventFetch = (() => {
24
+ if (!platform.fetch || !server.current) return
25
+ try {
26
+ const url = new URL(server.current.http.url)
27
+ const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1"
28
+ if (url.protocol === "http:" && !loopback) return platform.fetch
29
+ } catch {
30
+ return
31
+ }
32
+ })()
33
+
34
+ const currentServer = server.current
35
+ if (!currentServer) throw new Error(language.t("error.globalSDK.noServerAvailable"))
36
+
37
+ const eventSdk = createSdkForServer({
38
+ signal: abort.signal,
39
+ fetch: eventFetch,
40
+ server: currentServer.http,
41
+ })
42
+ const emitter = createGlobalEmitter<{
43
+ [key: string]: Event
44
+ }>()
45
+
46
+ type Queued = { directory: string; payload: Event }
47
+ const FLUSH_FRAME_MS = 16
48
+ const STREAM_YIELD_MS = 8
49
+ const RECONNECT_DELAY_MS = 250
50
+
51
+ let queue: Queued[] = []
52
+ let buffer: Queued[] = []
53
+ const coalesced = new Map<string, number>()
54
+ const staleDeltas = new Set<string>()
55
+ let timer: ReturnType<typeof setTimeout> | undefined
56
+ let last = 0
57
+
58
+ const deltaKey = (directory: string, messageID: string, partID: string) => `${directory}:${messageID}:${partID}`
59
+
60
+ const key = (directory: string, payload: Event) => {
61
+ if (payload.type === "session.status") return `session.status:${directory}:${payload.properties.sessionID}`
62
+ if (payload.type === "lsp.updated") return `lsp.updated:${directory}`
63
+ if (payload.type === "message.part.updated") {
64
+ const part = payload.properties.part
65
+ return `message.part.updated:${directory}:${part.messageID}:${part.id}`
66
+ }
67
+ }
68
+
69
+ const flush = () => {
70
+ if (timer) clearTimeout(timer)
71
+ timer = undefined
72
+
73
+ if (queue.length === 0) return
74
+
75
+ const events = queue
76
+ const skip = staleDeltas.size > 0 ? new Set(staleDeltas) : undefined
77
+ queue = buffer
78
+ buffer = events
79
+ queue.length = 0
80
+ coalesced.clear()
81
+ staleDeltas.clear()
82
+
83
+ last = Date.now()
84
+ batch(() => {
85
+ for (const event of events) {
86
+ if (skip && event.payload.type === "message.part.delta") {
87
+ const props = event.payload.properties
88
+ if (skip.has(deltaKey(event.directory, props.messageID, props.partID))) continue
89
+ }
90
+ emitter.emit(event.directory, event.payload)
91
+ }
92
+ })
93
+
94
+ buffer.length = 0
95
+ }
96
+
97
+ const schedule = () => {
98
+ if (timer) return
99
+ const elapsed = Date.now() - last
100
+ timer = setTimeout(flush, Math.max(0, FLUSH_FRAME_MS - elapsed))
101
+ }
102
+
103
+ let streamErrorLogged = false
104
+ const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
105
+ const aborted = (error: unknown) => abortError.safeParse(error).success
106
+
107
+ let attempt: AbortController | undefined
108
+ const HEARTBEAT_TIMEOUT_MS = 15_000
109
+ let lastEventAt = Date.now()
110
+ let heartbeat: ReturnType<typeof setTimeout> | undefined
111
+ const resetHeartbeat = () => {
112
+ lastEventAt = Date.now()
113
+ if (heartbeat) clearTimeout(heartbeat)
114
+ heartbeat = setTimeout(() => {
115
+ attempt?.abort()
116
+ }, HEARTBEAT_TIMEOUT_MS)
117
+ }
118
+ const clearHeartbeat = () => {
119
+ if (!heartbeat) return
120
+ clearTimeout(heartbeat)
121
+ heartbeat = undefined
122
+ }
123
+
124
+ void (async () => {
125
+ while (!abort.signal.aborted) {
126
+ attempt = new AbortController()
127
+ lastEventAt = Date.now()
128
+ const onAbort = () => {
129
+ attempt?.abort()
130
+ }
131
+ abort.signal.addEventListener("abort", onAbort)
132
+ try {
133
+ const events = await eventSdk.global.event({
134
+ signal: attempt.signal,
135
+ onSseError: (error) => {
136
+ if (aborted(error)) return
137
+ if (streamErrorLogged) return
138
+ streamErrorLogged = true
139
+ console.error("[global-sdk] event stream error", {
140
+ url: currentServer.http.url,
141
+ fetch: eventFetch ? "platform" : "webview",
142
+ error,
143
+ })
144
+ },
145
+ })
146
+ let yielded = Date.now()
147
+ resetHeartbeat()
148
+ for await (const event of events.stream) {
149
+ resetHeartbeat()
150
+ streamErrorLogged = false
151
+ const directory = event.directory ?? "global"
152
+ const payload = event.payload
153
+ const k = key(directory, payload)
154
+ if (k) {
155
+ const i = coalesced.get(k)
156
+ if (i !== undefined) {
157
+ queue[i] = { directory, payload }
158
+ if (payload.type === "message.part.updated") {
159
+ const part = payload.properties.part
160
+ staleDeltas.add(deltaKey(directory, part.messageID, part.id))
161
+ }
162
+ continue
163
+ }
164
+ coalesced.set(k, queue.length)
165
+ }
166
+ queue.push({ directory, payload })
167
+ schedule()
168
+
169
+ if (Date.now() - yielded < STREAM_YIELD_MS) continue
170
+ yielded = Date.now()
171
+ await wait(0)
172
+ }
173
+ } catch (error) {
174
+ if (!aborted(error) && !streamErrorLogged) {
175
+ streamErrorLogged = true
176
+ console.error("[global-sdk] event stream failed", {
177
+ url: currentServer.http.url,
178
+ fetch: eventFetch ? "platform" : "webview",
179
+ error,
180
+ })
181
+ }
182
+ } finally {
183
+ abort.signal.removeEventListener("abort", onAbort)
184
+ attempt = undefined
185
+ clearHeartbeat()
186
+ }
187
+
188
+ if (abort.signal.aborted) return
189
+ await wait(RECONNECT_DELAY_MS)
190
+ }
191
+ })().finally(flush)
192
+
193
+ const onVisibility = () => {
194
+ if (typeof document === "undefined") return
195
+ if (document.visibilityState !== "visible") return
196
+ if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return
197
+ attempt?.abort()
198
+ }
199
+ if (typeof document !== "undefined") {
200
+ document.addEventListener("visibilitychange", onVisibility)
201
+ }
202
+
203
+ onCleanup(() => {
204
+ if (typeof document !== "undefined") {
205
+ document.removeEventListener("visibilitychange", onVisibility)
206
+ }
207
+ abort.abort()
208
+ flush()
209
+ })
210
+
211
+ const sdk = createSdkForServer({
212
+ server: server.current.http,
213
+ fetch: platform.fetch,
214
+ throwOnError: true,
215
+ })
216
+
217
+ return {
218
+ url: currentServer.http.url,
219
+ client: sdk,
220
+ event: emitter,
221
+ createClient(opts: Omit<Parameters<typeof createSdkForServer>[0], "server" | "fetch">) {
222
+ const s = server.current
223
+ if (!s) throw new Error(language.t("error.globalSDK.serverNotAvailable"))
224
+ return createSdkForServer({
225
+ server: s.http,
226
+ fetch: platform.fetch,
227
+ ...opts,
228
+ })
229
+ },
230
+ }
231
+ },
232
+ })