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,96 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import {
3
+ clearSessionPrefetch,
4
+ clearSessionPrefetchDirectory,
5
+ getSessionPrefetch,
6
+ runSessionPrefetch,
7
+ setSessionPrefetch,
8
+ shouldSkipSessionPrefetch,
9
+ } from "./session-prefetch"
10
+
11
+ describe("session prefetch", () => {
12
+ test("stores and clears message metadata by directory", () => {
13
+ clearSessionPrefetch("/tmp/a", ["ses_1"])
14
+ clearSessionPrefetch("/tmp/b", ["ses_1"])
15
+
16
+ setSessionPrefetch({
17
+ directory: "/tmp/a",
18
+ sessionID: "ses_1",
19
+ limit: 200,
20
+ cursor: "abc",
21
+ complete: false,
22
+ at: 123,
23
+ })
24
+
25
+ expect(getSessionPrefetch("/tmp/a", "ses_1")).toEqual({ limit: 200, cursor: "abc", complete: false, at: 123 })
26
+ expect(getSessionPrefetch("/tmp/b", "ses_1")).toBeUndefined()
27
+
28
+ clearSessionPrefetch("/tmp/a", ["ses_1"])
29
+
30
+ expect(getSessionPrefetch("/tmp/a", "ses_1")).toBeUndefined()
31
+ })
32
+
33
+ test("dedupes inflight work", async () => {
34
+ clearSessionPrefetch("/tmp/c", ["ses_2"])
35
+
36
+ let calls = 0
37
+ const run = () =>
38
+ runSessionPrefetch({
39
+ directory: "/tmp/c",
40
+ sessionID: "ses_2",
41
+ task: async () => {
42
+ calls += 1
43
+ return { limit: 100, cursor: "next", complete: true, at: 456 }
44
+ },
45
+ })
46
+
47
+ const [a, b] = await Promise.all([run(), run()])
48
+
49
+ expect(calls).toBe(1)
50
+ expect(a).toEqual({ limit: 100, cursor: "next", complete: true, at: 456 })
51
+ expect(b).toEqual({ limit: 100, cursor: "next", complete: true, at: 456 })
52
+ })
53
+
54
+ test("clears a whole directory", () => {
55
+ setSessionPrefetch({ directory: "/tmp/d", sessionID: "ses_1", limit: 10, cursor: "a", complete: true, at: 1 })
56
+ setSessionPrefetch({ directory: "/tmp/d", sessionID: "ses_2", limit: 20, cursor: "b", complete: false, at: 2 })
57
+ setSessionPrefetch({ directory: "/tmp/e", sessionID: "ses_1", limit: 30, cursor: "c", complete: true, at: 3 })
58
+
59
+ clearSessionPrefetchDirectory("/tmp/d")
60
+
61
+ expect(getSessionPrefetch("/tmp/d", "ses_1")).toBeUndefined()
62
+ expect(getSessionPrefetch("/tmp/d", "ses_2")).toBeUndefined()
63
+ expect(getSessionPrefetch("/tmp/e", "ses_1")).toEqual({ limit: 30, cursor: "c", complete: true, at: 3 })
64
+ })
65
+
66
+ test("refreshes stale first-page prefetched history", () => {
67
+ expect(
68
+ shouldSkipSessionPrefetch({
69
+ message: true,
70
+ info: { limit: 200, cursor: "x", complete: false, at: 1 },
71
+ chunk: 200,
72
+ now: 1 + 15_001,
73
+ }),
74
+ ).toBe(false)
75
+ })
76
+
77
+ test("keeps deeper or complete history cached", () => {
78
+ expect(
79
+ shouldSkipSessionPrefetch({
80
+ message: true,
81
+ info: { limit: 400, cursor: "x", complete: false, at: 1 },
82
+ chunk: 200,
83
+ now: 1 + 15_001,
84
+ }),
85
+ ).toBe(true)
86
+
87
+ expect(
88
+ shouldSkipSessionPrefetch({
89
+ message: true,
90
+ info: { limit: 120, complete: true, at: 1 },
91
+ chunk: 200,
92
+ now: 1 + 15_001,
93
+ }),
94
+ ).toBe(true)
95
+ })
96
+ })
@@ -0,0 +1,100 @@
1
+ const key = (directory: string, sessionID: string) => `${directory}\n${sessionID}`
2
+
3
+ export const SESSION_PREFETCH_TTL = 15_000
4
+
5
+ type Meta = {
6
+ limit: number
7
+ cursor?: string
8
+ complete: boolean
9
+ at: number
10
+ }
11
+
12
+ export function shouldSkipSessionPrefetch(input: { message: boolean; info?: Meta; chunk: number; now?: number }) {
13
+ if (input.message) {
14
+ if (!input.info) return true
15
+ if (input.info.complete) return true
16
+ if (input.info.limit > input.chunk) return true
17
+ } else {
18
+ if (!input.info) return false
19
+ }
20
+
21
+ return (input.now ?? Date.now()) - input.info.at < SESSION_PREFETCH_TTL
22
+ }
23
+
24
+ const cache = new Map<string, Meta>()
25
+ const inflight = new Map<string, Promise<Meta | undefined>>()
26
+ const rev = new Map<string, number>()
27
+
28
+ const version = (id: string) => rev.get(id) ?? 0
29
+
30
+ export function getSessionPrefetch(directory: string, sessionID: string) {
31
+ return cache.get(key(directory, sessionID))
32
+ }
33
+
34
+ export function getSessionPrefetchPromise(directory: string, sessionID: string) {
35
+ return inflight.get(key(directory, sessionID))
36
+ }
37
+
38
+ export function clearSessionPrefetchInflight() {
39
+ inflight.clear()
40
+ }
41
+
42
+ export function isSessionPrefetchCurrent(directory: string, sessionID: string, value: number) {
43
+ return version(key(directory, sessionID)) === value
44
+ }
45
+
46
+ export function runSessionPrefetch(input: {
47
+ directory: string
48
+ sessionID: string
49
+ task: (value: number) => Promise<Meta | undefined>
50
+ }) {
51
+ const id = key(input.directory, input.sessionID)
52
+ const pending = inflight.get(id)
53
+ if (pending) return pending
54
+
55
+ const value = version(id)
56
+
57
+ const promise = input.task(value).finally(() => {
58
+ if (inflight.get(id) === promise) inflight.delete(id)
59
+ })
60
+
61
+ inflight.set(id, promise)
62
+ return promise
63
+ }
64
+
65
+ export function setSessionPrefetch(input: {
66
+ directory: string
67
+ sessionID: string
68
+ limit: number
69
+ cursor?: string
70
+ complete: boolean
71
+ at?: number
72
+ }) {
73
+ cache.set(key(input.directory, input.sessionID), {
74
+ limit: input.limit,
75
+ cursor: input.cursor,
76
+ complete: input.complete,
77
+ at: input.at ?? Date.now(),
78
+ })
79
+ }
80
+
81
+ export function clearSessionPrefetch(directory: string, sessionIDs: Iterable<string>) {
82
+ for (const sessionID of sessionIDs) {
83
+ if (!sessionID) continue
84
+ const id = key(directory, sessionID)
85
+ rev.set(id, version(id) + 1)
86
+ cache.delete(id)
87
+ inflight.delete(id)
88
+ }
89
+ }
90
+
91
+ export function clearSessionPrefetchDirectory(directory: string) {
92
+ const prefix = `${directory}\n`
93
+ const keys = new Set([...cache.keys(), ...inflight.keys()])
94
+ for (const id of keys) {
95
+ if (!id.startsWith(prefix)) continue
96
+ rev.set(id, version(id) + 1)
97
+ cache.delete(id)
98
+ inflight.delete(id)
99
+ }
100
+ }
@@ -0,0 +1,59 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import type { PermissionRequest, Session } from "@reign-labs/sdk/v2/client"
3
+ import { trimSessions } from "./session-trim"
4
+
5
+ const session = (input: { id: string; parentID?: string; created: number; updated?: number; archived?: number }) =>
6
+ ({
7
+ id: input.id,
8
+ parentID: input.parentID,
9
+ time: {
10
+ created: input.created,
11
+ updated: input.updated,
12
+ archived: input.archived,
13
+ },
14
+ }) as Session
15
+
16
+ describe("trimSessions", () => {
17
+ test("keeps base roots and recent roots beyond the limit", () => {
18
+ const now = 1_000_000
19
+ const list = [
20
+ session({ id: "a", created: now - 100_000 }),
21
+ session({ id: "b", created: now - 90_000 }),
22
+ session({ id: "c", created: now - 80_000 }),
23
+ session({ id: "d", created: now - 70_000, updated: now - 1_000 }),
24
+ session({ id: "e", created: now - 60_000, archived: now - 10 }),
25
+ ]
26
+
27
+ const result = trimSessions(list, { limit: 2, permission: {}, now })
28
+ expect(result.map((x) => x.id)).toEqual(["a", "b", "c", "d"])
29
+ })
30
+
31
+ test("keeps children when root is kept, permission exists, or child is recent", () => {
32
+ const now = 1_000_000
33
+ const list = [
34
+ session({ id: "root-1", created: now - 1000 }),
35
+ session({ id: "root-2", created: now - 2000 }),
36
+ session({ id: "z-root", created: now - 30_000_000 }),
37
+ session({ id: "child-kept-by-root", parentID: "root-1", created: now - 20_000_000 }),
38
+ session({ id: "child-kept-by-permission", parentID: "z-root", created: now - 20_000_000 }),
39
+ session({ id: "child-kept-by-recency", parentID: "z-root", created: now - 500 }),
40
+ session({ id: "child-trimmed", parentID: "z-root", created: now - 20_000_000 }),
41
+ ]
42
+
43
+ const result = trimSessions(list, {
44
+ limit: 2,
45
+ permission: {
46
+ "child-kept-by-permission": [{ id: "perm-1" } as PermissionRequest],
47
+ },
48
+ now,
49
+ })
50
+
51
+ expect(result.map((x) => x.id)).toEqual([
52
+ "child-kept-by-permission",
53
+ "child-kept-by-recency",
54
+ "child-kept-by-root",
55
+ "root-1",
56
+ "root-2",
57
+ ])
58
+ })
59
+ })
@@ -0,0 +1,56 @@
1
+ import type { PermissionRequest, Session } from "@reign-labs/sdk/v2/client"
2
+ import { cmp } from "./utils"
3
+ import { SESSION_RECENT_LIMIT, SESSION_RECENT_WINDOW } from "./types"
4
+
5
+ export function sessionUpdatedAt(session: Session) {
6
+ return session.time.updated ?? session.time.created
7
+ }
8
+
9
+ export function compareSessionRecent(a: Session, b: Session) {
10
+ const aUpdated = sessionUpdatedAt(a)
11
+ const bUpdated = sessionUpdatedAt(b)
12
+ if (aUpdated !== bUpdated) return bUpdated - aUpdated
13
+ return cmp(a.id, b.id)
14
+ }
15
+
16
+ export function takeRecentSessions(sessions: Session[], limit: number, cutoff: number) {
17
+ if (limit <= 0) return [] as Session[]
18
+ const selected: Session[] = []
19
+ const seen = new Set<string>()
20
+ for (const session of sessions) {
21
+ if (!session?.id) continue
22
+ if (seen.has(session.id)) continue
23
+ seen.add(session.id)
24
+ if (sessionUpdatedAt(session) <= cutoff) continue
25
+ const index = selected.findIndex((x) => compareSessionRecent(session, x) < 0)
26
+ if (index === -1) selected.push(session)
27
+ if (index !== -1) selected.splice(index, 0, session)
28
+ if (selected.length > limit) selected.pop()
29
+ }
30
+ return selected
31
+ }
32
+
33
+ export function trimSessions(
34
+ input: Session[],
35
+ options: { limit: number; permission: Record<string, PermissionRequest[]>; now?: number },
36
+ ) {
37
+ const limit = Math.max(0, options.limit)
38
+ const cutoff = (options.now ?? Date.now()) - SESSION_RECENT_WINDOW
39
+ const all = input
40
+ .filter((s) => !!s?.id)
41
+ .filter((s) => !s.time?.archived)
42
+ .sort((a, b) => cmp(a.id, b.id))
43
+ const roots = all.filter((s) => !s.parentID)
44
+ const children = all.filter((s) => !!s.parentID)
45
+ const base = roots.slice(0, limit)
46
+ const recent = takeRecentSessions(roots.slice(limit), SESSION_RECENT_LIMIT, cutoff)
47
+ const keepRoots = [...base, ...recent]
48
+ const keepRootIds = new Set(keepRoots.map((s) => s.id))
49
+ const keepChildren = children.filter((s) => {
50
+ if (s.parentID && keepRootIds.has(s.parentID)) return true
51
+ const perms = options.permission[s.id] ?? []
52
+ if (perms.length > 0) return true
53
+ return sessionUpdatedAt(s) > cutoff
54
+ })
55
+ return [...keepRoots, ...keepChildren].sort((a, b) => cmp(a.id, b.id))
56
+ }
@@ -0,0 +1,133 @@
1
+ import type {
2
+ Agent,
3
+ Command,
4
+ Config,
5
+ FileDiff,
6
+ LspStatus,
7
+ McpStatus,
8
+ Message,
9
+ Part,
10
+ Path,
11
+ PermissionRequest,
12
+ Project,
13
+ ProviderListResponse,
14
+ QuestionRequest,
15
+ Session,
16
+ SessionStatus,
17
+ Todo,
18
+ VcsInfo,
19
+ } from "@reign-labs/sdk/v2/client"
20
+ import type { Accessor } from "solid-js"
21
+ import type { SetStoreFunction, Store } from "solid-js/store"
22
+
23
+ export type ProjectMeta = {
24
+ name?: string
25
+ icon?: {
26
+ override?: string
27
+ color?: string
28
+ }
29
+ commands?: {
30
+ start?: string
31
+ }
32
+ }
33
+
34
+ export type State = {
35
+ status: "loading" | "partial" | "complete"
36
+ agent: Agent[]
37
+ command: Command[]
38
+ project: string
39
+ projectMeta: ProjectMeta | undefined
40
+ icon: string | undefined
41
+ provider: ProviderListResponse
42
+ config: Config
43
+ path: Path
44
+ session: Session[]
45
+ sessionTotal: number
46
+ session_status: {
47
+ [sessionID: string]: SessionStatus
48
+ }
49
+ session_diff: {
50
+ [sessionID: string]: FileDiff[]
51
+ }
52
+ todo: {
53
+ [sessionID: string]: Todo[]
54
+ }
55
+ permission: {
56
+ [sessionID: string]: PermissionRequest[]
57
+ }
58
+ question: {
59
+ [sessionID: string]: QuestionRequest[]
60
+ }
61
+ mcp: {
62
+ [name: string]: McpStatus
63
+ }
64
+ lsp: LspStatus[]
65
+ vcs: VcsInfo | undefined
66
+ limit: number
67
+ message: {
68
+ [sessionID: string]: Message[]
69
+ }
70
+ part: {
71
+ [messageID: string]: Part[]
72
+ }
73
+ }
74
+
75
+ export type VcsCache = {
76
+ store: Store<{ value: VcsInfo | undefined }>
77
+ setStore: SetStoreFunction<{ value: VcsInfo | undefined }>
78
+ ready: Accessor<boolean>
79
+ }
80
+
81
+ export type MetaCache = {
82
+ store: Store<{ value: ProjectMeta | undefined }>
83
+ setStore: SetStoreFunction<{ value: ProjectMeta | undefined }>
84
+ ready: Accessor<boolean>
85
+ }
86
+
87
+ export type IconCache = {
88
+ store: Store<{ value: string | undefined }>
89
+ setStore: SetStoreFunction<{ value: string | undefined }>
90
+ ready: Accessor<boolean>
91
+ }
92
+
93
+ export type ChildOptions = {
94
+ bootstrap?: boolean
95
+ }
96
+
97
+ export type DirState = {
98
+ lastAccessAt: number
99
+ }
100
+
101
+ export type EvictPlan = {
102
+ stores: string[]
103
+ state: Map<string, DirState>
104
+ pins: Set<string>
105
+ max: number
106
+ ttl: number
107
+ now: number
108
+ }
109
+
110
+ export type DisposeCheck = {
111
+ directory: string
112
+ hasStore: boolean
113
+ pinned: boolean
114
+ booting: boolean
115
+ loadingSessions: boolean
116
+ }
117
+
118
+ export type RootLoadArgs = {
119
+ directory: string
120
+ limit: number
121
+ list: (query: { directory: string; roots: true; limit?: number }) => Promise<{ data?: Session[] }>
122
+ }
123
+
124
+ export type RootLoadResult = {
125
+ data?: Session[]
126
+ limit: number
127
+ limited: boolean
128
+ }
129
+
130
+ export const MAX_DIR_STORES = 30
131
+ export const DIR_IDLE_TTL_MS = 20 * 60 * 1000
132
+ export const SESSION_RECENT_WINDOW = 4 * 60 * 60 * 1000
133
+ export const SESSION_RECENT_LIMIT = 50
@@ -0,0 +1,25 @@
1
+ import type { Project, ProviderListResponse } from "@reign-labs/sdk/v2/client"
2
+
3
+ export const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
4
+
5
+ export function normalizeProviderList(input: ProviderListResponse): ProviderListResponse {
6
+ return {
7
+ ...input,
8
+ all: input.all.map((provider) => ({
9
+ ...provider,
10
+ models: Object.fromEntries(Object.entries(provider.models).filter(([, info]) => info.status !== "deprecated")),
11
+ })),
12
+ }
13
+ }
14
+
15
+ export function sanitizeProject(project: Project) {
16
+ if (!project.icon?.url && !project.icon?.override) return project
17
+ return {
18
+ ...project,
19
+ icon: {
20
+ ...project.icon,
21
+ url: undefined,
22
+ override: undefined,
23
+ },
24
+ }
25
+ }
@@ -0,0 +1,122 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { canDisposeDirectory, pickDirectoriesToEvict } from "./global-sync/eviction"
3
+ import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global-sync/session-load"
4
+
5
+ describe("pickDirectoriesToEvict", () => {
6
+ test("keeps pinned stores and evicts idle stores", () => {
7
+ const now = 5_000
8
+ const picks = pickDirectoriesToEvict({
9
+ stores: ["a", "b", "c", "d"],
10
+ state: new Map([
11
+ ["a", { lastAccessAt: 1_000 }],
12
+ ["b", { lastAccessAt: 4_900 }],
13
+ ["c", { lastAccessAt: 4_800 }],
14
+ ["d", { lastAccessAt: 3_000 }],
15
+ ]),
16
+ pins: new Set(["a"]),
17
+ max: 2,
18
+ ttl: 1_500,
19
+ now,
20
+ })
21
+
22
+ expect(picks).toEqual(["d", "c"])
23
+ })
24
+ })
25
+
26
+ describe("loadRootSessionsWithFallback", () => {
27
+ test("uses limited roots query when supported", async () => {
28
+ const calls: Array<{ directory: string; roots: true; limit?: number }> = []
29
+
30
+ const result = await loadRootSessionsWithFallback({
31
+ directory: "dir",
32
+ limit: 10,
33
+ list: async (query) => {
34
+ calls.push(query)
35
+ return { data: [] }
36
+ },
37
+ })
38
+
39
+ expect(result.data).toEqual([])
40
+ expect(result.limited).toBe(true)
41
+ expect(calls).toEqual([{ directory: "dir", roots: true, limit: 10 }])
42
+ })
43
+
44
+ test("falls back to full roots query on limited-query failure", async () => {
45
+ const calls: Array<{ directory: string; roots: true; limit?: number }> = []
46
+
47
+ const result = await loadRootSessionsWithFallback({
48
+ directory: "dir",
49
+ limit: 25,
50
+ list: async (query) => {
51
+ calls.push(query)
52
+ if (query.limit) throw new Error("unsupported")
53
+ return { data: [] }
54
+ },
55
+ })
56
+
57
+ expect(result.data).toEqual([])
58
+ expect(result.limited).toBe(false)
59
+ expect(calls).toEqual([
60
+ { directory: "dir", roots: true, limit: 25 },
61
+ { directory: "dir", roots: true },
62
+ ])
63
+ })
64
+ })
65
+
66
+ describe("estimateRootSessionTotal", () => {
67
+ test("keeps exact total for full fetches", () => {
68
+ expect(estimateRootSessionTotal({ count: 42, limit: 10, limited: false })).toBe(42)
69
+ })
70
+
71
+ test("marks has-more for full-limit limited fetches", () => {
72
+ expect(estimateRootSessionTotal({ count: 10, limit: 10, limited: true })).toBe(11)
73
+ })
74
+
75
+ test("keeps exact total when limited fetch is under limit", () => {
76
+ expect(estimateRootSessionTotal({ count: 9, limit: 10, limited: true })).toBe(9)
77
+ })
78
+ })
79
+
80
+ describe("canDisposeDirectory", () => {
81
+ test("rejects pinned or inflight directories", () => {
82
+ expect(
83
+ canDisposeDirectory({
84
+ directory: "dir",
85
+ hasStore: true,
86
+ pinned: true,
87
+ booting: false,
88
+ loadingSessions: false,
89
+ }),
90
+ ).toBe(false)
91
+ expect(
92
+ canDisposeDirectory({
93
+ directory: "dir",
94
+ hasStore: true,
95
+ pinned: false,
96
+ booting: true,
97
+ loadingSessions: false,
98
+ }),
99
+ ).toBe(false)
100
+ expect(
101
+ canDisposeDirectory({
102
+ directory: "dir",
103
+ hasStore: true,
104
+ pinned: false,
105
+ booting: false,
106
+ loadingSessions: true,
107
+ }),
108
+ ).toBe(false)
109
+ })
110
+
111
+ test("accepts idle unpinned directory store", () => {
112
+ expect(
113
+ canDisposeDirectory({
114
+ directory: "dir",
115
+ hasStore: true,
116
+ pinned: false,
117
+ booting: false,
118
+ loadingSessions: false,
119
+ }),
120
+ ).toBe(true)
121
+ })
122
+ })