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,359 @@
1
+ import { Binary } from "@reign-labs/util/binary"
2
+ import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
3
+ import type {
4
+ FileDiff,
5
+ Message,
6
+ Part,
7
+ PermissionRequest,
8
+ Project,
9
+ QuestionRequest,
10
+ Session,
11
+ SessionStatus,
12
+ Todo,
13
+ } from "@reign-labs/sdk/v2/client"
14
+ import type { State, VcsCache } from "./types"
15
+ import { trimSessions } from "./session-trim"
16
+ import { dropSessionCaches } from "./session-cache"
17
+
18
+ const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
19
+
20
+ export function applyGlobalEvent(input: {
21
+ event: { type: string; properties?: unknown }
22
+ project: Project[]
23
+ setGlobalProject: (next: Project[] | ((draft: Project[]) => void)) => void
24
+ refresh: () => void
25
+ }) {
26
+ if (input.event.type === "global.disposed" || input.event.type === "server.connected") {
27
+ input.refresh()
28
+ return
29
+ }
30
+
31
+ if (input.event.type !== "project.updated") return
32
+ const properties = input.event.properties as Project
33
+ const result = Binary.search(input.project, properties.id, (s) => s.id)
34
+ if (result.found) {
35
+ input.setGlobalProject((draft) => {
36
+ draft[result.index] = { ...draft[result.index], ...properties }
37
+ })
38
+ return
39
+ }
40
+ input.setGlobalProject((draft) => {
41
+ draft.splice(result.index, 0, properties)
42
+ })
43
+ }
44
+
45
+ function cleanupSessionCaches(
46
+ setStore: SetStoreFunction<State>,
47
+ sessionID: string,
48
+ setSessionTodo?: (sessionID: string, todos: Todo[] | undefined) => void,
49
+ ) {
50
+ if (!sessionID) return
51
+ setSessionTodo?.(sessionID, undefined)
52
+ setStore(
53
+ produce((draft) => {
54
+ dropSessionCaches(draft, [sessionID])
55
+ }),
56
+ )
57
+ }
58
+
59
+ export function cleanupDroppedSessionCaches(
60
+ store: Store<State>,
61
+ setStore: SetStoreFunction<State>,
62
+ next: Session[],
63
+ setSessionTodo?: (sessionID: string, todos: Todo[] | undefined) => void,
64
+ ) {
65
+ const keep = new Set(next.map((item) => item.id))
66
+ const stale = [
67
+ ...Object.keys(store.message),
68
+ ...Object.keys(store.session_diff),
69
+ ...Object.keys(store.todo),
70
+ ...Object.keys(store.permission),
71
+ ...Object.keys(store.question),
72
+ ...Object.keys(store.session_status),
73
+ ...Object.values(store.part)
74
+ .map((parts) => parts?.find((part) => !!part?.sessionID)?.sessionID)
75
+ .filter((sessionID): sessionID is string => !!sessionID),
76
+ ].filter((sessionID, index, list) => !keep.has(sessionID) && list.indexOf(sessionID) === index)
77
+ if (stale.length === 0) return
78
+ for (const sessionID of stale) {
79
+ setSessionTodo?.(sessionID, undefined)
80
+ }
81
+ setStore(
82
+ produce((draft) => {
83
+ dropSessionCaches(draft, stale)
84
+ }),
85
+ )
86
+ }
87
+
88
+ export function applyDirectoryEvent(input: {
89
+ event: { type: string; properties?: unknown }
90
+ store: Store<State>
91
+ setStore: SetStoreFunction<State>
92
+ push: (directory: string) => void
93
+ directory: string
94
+ loadLsp: () => void
95
+ vcsCache?: VcsCache
96
+ setSessionTodo?: (sessionID: string, todos: Todo[] | undefined) => void
97
+ }) {
98
+ const event = input.event
99
+ switch (event.type) {
100
+ case "server.instance.disposed": {
101
+ input.push(input.directory)
102
+ return
103
+ }
104
+ case "session.created": {
105
+ const info = (event.properties as { info: Session }).info
106
+ const result = Binary.search(input.store.session, info.id, (s) => s.id)
107
+ if (result.found) {
108
+ input.setStore("session", result.index, reconcile(info))
109
+ break
110
+ }
111
+ const next = input.store.session.slice()
112
+ next.splice(result.index, 0, info)
113
+ const trimmed = trimSessions(next, { limit: input.store.limit, permission: input.store.permission })
114
+ input.setStore("session", reconcile(trimmed, { key: "id" }))
115
+ cleanupDroppedSessionCaches(input.store, input.setStore, trimmed, input.setSessionTodo)
116
+ if (!info.parentID) input.setStore("sessionTotal", (value) => value + 1)
117
+ break
118
+ }
119
+ case "session.updated": {
120
+ const info = (event.properties as { info: Session }).info
121
+ const result = Binary.search(input.store.session, info.id, (s) => s.id)
122
+ if (info.time.archived) {
123
+ if (result.found) {
124
+ input.setStore(
125
+ "session",
126
+ produce((draft) => {
127
+ draft.splice(result.index, 1)
128
+ }),
129
+ )
130
+ }
131
+ cleanupSessionCaches(input.setStore, info.id, input.setSessionTodo)
132
+ if (info.parentID) break
133
+ input.setStore("sessionTotal", (value) => Math.max(0, value - 1))
134
+ break
135
+ }
136
+ if (result.found) {
137
+ input.setStore("session", result.index, reconcile(info))
138
+ break
139
+ }
140
+ const next = input.store.session.slice()
141
+ next.splice(result.index, 0, info)
142
+ const trimmed = trimSessions(next, { limit: input.store.limit, permission: input.store.permission })
143
+ input.setStore("session", reconcile(trimmed, { key: "id" }))
144
+ cleanupDroppedSessionCaches(input.store, input.setStore, trimmed, input.setSessionTodo)
145
+ break
146
+ }
147
+ case "session.deleted": {
148
+ const info = (event.properties as { info: Session }).info
149
+ const result = Binary.search(input.store.session, info.id, (s) => s.id)
150
+ if (result.found) {
151
+ input.setStore(
152
+ "session",
153
+ produce((draft) => {
154
+ draft.splice(result.index, 1)
155
+ }),
156
+ )
157
+ }
158
+ cleanupSessionCaches(input.setStore, info.id, input.setSessionTodo)
159
+ if (info.parentID) break
160
+ input.setStore("sessionTotal", (value) => Math.max(0, value - 1))
161
+ break
162
+ }
163
+ case "session.diff": {
164
+ const props = event.properties as { sessionID: string; diff: FileDiff[] }
165
+ input.setStore("session_diff", props.sessionID, reconcile(props.diff, { key: "file" }))
166
+ break
167
+ }
168
+ case "todo.updated": {
169
+ const props = event.properties as { sessionID: string; todos: Todo[] }
170
+ input.setStore("todo", props.sessionID, reconcile(props.todos, { key: "id" }))
171
+ input.setSessionTodo?.(props.sessionID, props.todos)
172
+ break
173
+ }
174
+ case "session.status": {
175
+ const props = event.properties as { sessionID: string; status: SessionStatus }
176
+ input.setStore("session_status", props.sessionID, reconcile(props.status))
177
+ break
178
+ }
179
+ case "message.updated": {
180
+ const info = (event.properties as { info: Message }).info
181
+ const messages = input.store.message[info.sessionID]
182
+ if (!messages) {
183
+ input.setStore("message", info.sessionID, [info])
184
+ break
185
+ }
186
+ const result = Binary.search(messages, info.id, (m) => m.id)
187
+ if (result.found) {
188
+ input.setStore("message", info.sessionID, result.index, reconcile(info))
189
+ break
190
+ }
191
+ input.setStore(
192
+ "message",
193
+ info.sessionID,
194
+ produce((draft) => {
195
+ draft.splice(result.index, 0, info)
196
+ }),
197
+ )
198
+ break
199
+ }
200
+ case "message.removed": {
201
+ const props = event.properties as { sessionID: string; messageID: string }
202
+ input.setStore(
203
+ produce((draft) => {
204
+ const messages = draft.message[props.sessionID]
205
+ if (messages) {
206
+ const result = Binary.search(messages, props.messageID, (m) => m.id)
207
+ if (result.found) messages.splice(result.index, 1)
208
+ }
209
+ delete draft.part[props.messageID]
210
+ }),
211
+ )
212
+ break
213
+ }
214
+ case "message.part.updated": {
215
+ const part = (event.properties as { part: Part }).part
216
+ if (SKIP_PARTS.has(part.type)) break
217
+ const parts = input.store.part[part.messageID]
218
+ if (!parts) {
219
+ input.setStore("part", part.messageID, [part])
220
+ break
221
+ }
222
+ const result = Binary.search(parts, part.id, (p) => p.id)
223
+ if (result.found) {
224
+ input.setStore("part", part.messageID, result.index, reconcile(part))
225
+ break
226
+ }
227
+ input.setStore(
228
+ "part",
229
+ part.messageID,
230
+ produce((draft) => {
231
+ draft.splice(result.index, 0, part)
232
+ }),
233
+ )
234
+ break
235
+ }
236
+ case "message.part.removed": {
237
+ const props = event.properties as { messageID: string; partID: string }
238
+ const parts = input.store.part[props.messageID]
239
+ if (!parts) break
240
+ const result = Binary.search(parts, props.partID, (p) => p.id)
241
+ if (result.found) {
242
+ input.setStore(
243
+ produce((draft) => {
244
+ const list = draft.part[props.messageID]
245
+ if (!list) return
246
+ const next = Binary.search(list, props.partID, (p) => p.id)
247
+ if (!next.found) return
248
+ list.splice(next.index, 1)
249
+ if (list.length === 0) delete draft.part[props.messageID]
250
+ }),
251
+ )
252
+ }
253
+ break
254
+ }
255
+ case "message.part.delta": {
256
+ const props = event.properties as { messageID: string; partID: string; field: string; delta: string }
257
+ const parts = input.store.part[props.messageID]
258
+ if (!parts) break
259
+ const result = Binary.search(parts, props.partID, (p) => p.id)
260
+ if (!result.found) break
261
+ input.setStore(
262
+ "part",
263
+ props.messageID,
264
+ produce((draft) => {
265
+ const part = draft[result.index]
266
+ const field = props.field as keyof typeof part
267
+ const existing = part[field] as string | undefined
268
+ ;(part[field] as string) = (existing ?? "") + props.delta
269
+ }),
270
+ )
271
+ break
272
+ }
273
+ case "vcs.branch.updated": {
274
+ const props = event.properties as { branch: string }
275
+ if (input.store.vcs?.branch === props.branch) break
276
+ const next = { branch: props.branch }
277
+ input.setStore("vcs", next)
278
+ if (input.vcsCache) input.vcsCache.setStore("value", next)
279
+ break
280
+ }
281
+ case "permission.asked": {
282
+ const permission = event.properties as PermissionRequest
283
+ const permissions = input.store.permission[permission.sessionID]
284
+ if (!permissions) {
285
+ input.setStore("permission", permission.sessionID, [permission])
286
+ break
287
+ }
288
+ const result = Binary.search(permissions, permission.id, (p) => p.id)
289
+ if (result.found) {
290
+ input.setStore("permission", permission.sessionID, result.index, reconcile(permission))
291
+ break
292
+ }
293
+ input.setStore(
294
+ "permission",
295
+ permission.sessionID,
296
+ produce((draft) => {
297
+ draft.splice(result.index, 0, permission)
298
+ }),
299
+ )
300
+ break
301
+ }
302
+ case "permission.replied": {
303
+ const props = event.properties as { sessionID: string; requestID: string }
304
+ const permissions = input.store.permission[props.sessionID]
305
+ if (!permissions) break
306
+ const result = Binary.search(permissions, props.requestID, (p) => p.id)
307
+ if (!result.found) break
308
+ input.setStore(
309
+ "permission",
310
+ props.sessionID,
311
+ produce((draft) => {
312
+ draft.splice(result.index, 1)
313
+ }),
314
+ )
315
+ break
316
+ }
317
+ case "question.asked": {
318
+ const question = event.properties as QuestionRequest
319
+ const questions = input.store.question[question.sessionID]
320
+ if (!questions) {
321
+ input.setStore("question", question.sessionID, [question])
322
+ break
323
+ }
324
+ const result = Binary.search(questions, question.id, (q) => q.id)
325
+ if (result.found) {
326
+ input.setStore("question", question.sessionID, result.index, reconcile(question))
327
+ break
328
+ }
329
+ input.setStore(
330
+ "question",
331
+ question.sessionID,
332
+ produce((draft) => {
333
+ draft.splice(result.index, 0, question)
334
+ }),
335
+ )
336
+ break
337
+ }
338
+ case "question.replied":
339
+ case "question.rejected": {
340
+ const props = event.properties as { sessionID: string; requestID: string }
341
+ const questions = input.store.question[props.sessionID]
342
+ if (!questions) break
343
+ const result = Binary.search(questions, props.requestID, (q) => q.id)
344
+ if (!result.found) break
345
+ input.setStore(
346
+ "question",
347
+ props.sessionID,
348
+ produce((draft) => {
349
+ draft.splice(result.index, 1)
350
+ }),
351
+ )
352
+ break
353
+ }
354
+ case "lsp.updated": {
355
+ input.loadLsp()
356
+ break
357
+ }
358
+ }
359
+ }
@@ -0,0 +1,28 @@
1
+ import type { DisposeCheck, EvictPlan } from "./types"
2
+
3
+ export function pickDirectoriesToEvict(input: EvictPlan) {
4
+ const overflow = Math.max(0, input.stores.length - input.max)
5
+ let pendingOverflow = overflow
6
+ const sorted = input.stores
7
+ .filter((dir) => !input.pins.has(dir))
8
+ .slice()
9
+ .sort((a, b) => (input.state.get(a)?.lastAccessAt ?? 0) - (input.state.get(b)?.lastAccessAt ?? 0))
10
+ const output: string[] = []
11
+ for (const dir of sorted) {
12
+ const last = input.state.get(dir)?.lastAccessAt ?? 0
13
+ const idle = input.now - last >= input.ttl
14
+ if (!idle && pendingOverflow <= 0) continue
15
+ output.push(dir)
16
+ if (pendingOverflow > 0) pendingOverflow -= 1
17
+ }
18
+ return output
19
+ }
20
+
21
+ export function canDisposeDirectory(input: DisposeCheck) {
22
+ if (!input.directory) return false
23
+ if (!input.hasStore) return false
24
+ if (input.pinned) return false
25
+ if (input.booting) return false
26
+ if (input.loadingSessions) return false
27
+ return true
28
+ }
@@ -0,0 +1,83 @@
1
+ type QueueInput = {
2
+ paused: () => boolean
3
+ bootstrap: () => Promise<void>
4
+ bootstrapInstance: (directory: string) => Promise<void> | void
5
+ }
6
+
7
+ export function createRefreshQueue(input: QueueInput) {
8
+ const queued = new Set<string>()
9
+ let root = false
10
+ let running = false
11
+ let timer: ReturnType<typeof setTimeout> | undefined
12
+
13
+ const tick = () => new Promise<void>((resolve) => setTimeout(resolve, 0))
14
+
15
+ const take = (count: number) => {
16
+ if (queued.size === 0) return [] as string[]
17
+ const items: string[] = []
18
+ for (const item of queued) {
19
+ queued.delete(item)
20
+ items.push(item)
21
+ if (items.length >= count) break
22
+ }
23
+ return items
24
+ }
25
+
26
+ const schedule = () => {
27
+ if (timer) return
28
+ timer = setTimeout(() => {
29
+ timer = undefined
30
+ void drain()
31
+ }, 0)
32
+ }
33
+
34
+ const push = (directory: string) => {
35
+ if (!directory) return
36
+ queued.add(directory)
37
+ if (input.paused()) return
38
+ schedule()
39
+ }
40
+
41
+ const refresh = () => {
42
+ root = true
43
+ if (input.paused()) return
44
+ schedule()
45
+ }
46
+
47
+ async function drain() {
48
+ if (running) return
49
+ running = true
50
+ try {
51
+ while (true) {
52
+ if (input.paused()) return
53
+ if (root) {
54
+ root = false
55
+ await input.bootstrap()
56
+ await tick()
57
+ continue
58
+ }
59
+ const dirs = take(2)
60
+ if (dirs.length === 0) return
61
+ await Promise.all(dirs.map((dir) => input.bootstrapInstance(dir)))
62
+ await tick()
63
+ }
64
+ } finally {
65
+ running = false
66
+ if (input.paused()) return
67
+ if (root || queued.size) schedule()
68
+ }
69
+ }
70
+
71
+ return {
72
+ push,
73
+ refresh,
74
+ clear(directory: string) {
75
+ queued.delete(directory)
76
+ },
77
+ dispose() {
78
+ if (!timer) return
79
+ clearTimeout(timer)
80
+ timer = undefined
81
+ },
82
+ }
83
+ }
@@ -0,0 +1,102 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import type {
3
+ FileDiff,
4
+ Message,
5
+ Part,
6
+ PermissionRequest,
7
+ QuestionRequest,
8
+ SessionStatus,
9
+ Todo,
10
+ } from "@reign-labs/sdk/v2/client"
11
+ import { dropSessionCaches, pickSessionCacheEvictions } from "./session-cache"
12
+
13
+ const msg = (id: string, sessionID: string) =>
14
+ ({
15
+ id,
16
+ sessionID,
17
+ role: "user",
18
+ time: { created: 1 },
19
+ agent: "assistant",
20
+ model: { providerID: "openai", modelID: "gpt" },
21
+ }) as Message
22
+
23
+ const part = (id: string, sessionID: string, messageID: string) =>
24
+ ({
25
+ id,
26
+ sessionID,
27
+ messageID,
28
+ type: "text",
29
+ text: id,
30
+ }) as Part
31
+
32
+ describe("app session cache", () => {
33
+ test("dropSessionCaches clears orphaned parts without message rows", () => {
34
+ const store: {
35
+ session_status: Record<string, SessionStatus | undefined>
36
+ session_diff: Record<string, FileDiff[] | undefined>
37
+ todo: Record<string, Todo[] | undefined>
38
+ message: Record<string, Message[] | undefined>
39
+ part: Record<string, Part[] | undefined>
40
+ permission: Record<string, PermissionRequest[] | undefined>
41
+ question: Record<string, QuestionRequest[] | undefined>
42
+ } = {
43
+ session_status: { ses_1: { type: "busy" } as SessionStatus },
44
+ session_diff: { ses_1: [] },
45
+ todo: { ses_1: [] as Todo[] },
46
+ message: {},
47
+ part: { msg_1: [part("prt_1", "ses_1", "msg_1")] },
48
+ permission: { ses_1: [] as PermissionRequest[] },
49
+ question: { ses_1: [] as QuestionRequest[] },
50
+ }
51
+
52
+ dropSessionCaches(store, ["ses_1"])
53
+
54
+ expect(store.message.ses_1).toBeUndefined()
55
+ expect(store.part.msg_1).toBeUndefined()
56
+ expect(store.todo.ses_1).toBeUndefined()
57
+ expect(store.session_diff.ses_1).toBeUndefined()
58
+ expect(store.session_status.ses_1).toBeUndefined()
59
+ expect(store.permission.ses_1).toBeUndefined()
60
+ expect(store.question.ses_1).toBeUndefined()
61
+ })
62
+
63
+ test("dropSessionCaches clears message-backed parts", () => {
64
+ const m = msg("msg_1", "ses_1")
65
+ const store: {
66
+ session_status: Record<string, SessionStatus | undefined>
67
+ session_diff: Record<string, FileDiff[] | undefined>
68
+ todo: Record<string, Todo[] | undefined>
69
+ message: Record<string, Message[] | undefined>
70
+ part: Record<string, Part[] | undefined>
71
+ permission: Record<string, PermissionRequest[] | undefined>
72
+ question: Record<string, QuestionRequest[] | undefined>
73
+ } = {
74
+ session_status: {},
75
+ session_diff: {},
76
+ todo: {},
77
+ message: { ses_1: [m] },
78
+ part: { [m.id]: [part("prt_1", "ses_1", m.id)] },
79
+ permission: {},
80
+ question: {},
81
+ }
82
+
83
+ dropSessionCaches(store, ["ses_1"])
84
+
85
+ expect(store.message.ses_1).toBeUndefined()
86
+ expect(store.part[m.id]).toBeUndefined()
87
+ })
88
+
89
+ test("pickSessionCacheEvictions preserves requested sessions", () => {
90
+ const seen = new Set(["ses_1", "ses_2", "ses_3"])
91
+
92
+ const stale = pickSessionCacheEvictions({
93
+ seen,
94
+ keep: "ses_4",
95
+ limit: 2,
96
+ preserve: ["ses_1"],
97
+ })
98
+
99
+ expect(stale).toEqual(["ses_2", "ses_3"])
100
+ expect([...seen]).toEqual(["ses_1", "ses_4"])
101
+ })
102
+ })
@@ -0,0 +1,62 @@
1
+ import type {
2
+ FileDiff,
3
+ Message,
4
+ Part,
5
+ PermissionRequest,
6
+ QuestionRequest,
7
+ SessionStatus,
8
+ Todo,
9
+ } from "@reign-labs/sdk/v2/client"
10
+
11
+ export const SESSION_CACHE_LIMIT = 40
12
+
13
+ type SessionCache = {
14
+ session_status: Record<string, SessionStatus | undefined>
15
+ session_diff: Record<string, FileDiff[] | undefined>
16
+ todo: Record<string, Todo[] | undefined>
17
+ message: Record<string, Message[] | undefined>
18
+ part: Record<string, Part[] | undefined>
19
+ permission: Record<string, PermissionRequest[] | undefined>
20
+ question: Record<string, QuestionRequest[] | undefined>
21
+ }
22
+
23
+ export function dropSessionCaches(store: SessionCache, sessionIDs: Iterable<string>) {
24
+ const stale = new Set(Array.from(sessionIDs).filter(Boolean))
25
+ if (stale.size === 0) return
26
+
27
+ for (const key of Object.keys(store.part)) {
28
+ const parts = store.part[key]
29
+ if (!parts?.some((part) => stale.has(part?.sessionID ?? ""))) continue
30
+ delete store.part[key]
31
+ }
32
+
33
+ for (const sessionID of stale) {
34
+ delete store.message[sessionID]
35
+ delete store.todo[sessionID]
36
+ delete store.session_diff[sessionID]
37
+ delete store.session_status[sessionID]
38
+ delete store.permission[sessionID]
39
+ delete store.question[sessionID]
40
+ }
41
+ }
42
+
43
+ export function pickSessionCacheEvictions(input: {
44
+ seen: Set<string>
45
+ keep: string
46
+ limit: number
47
+ preserve?: Iterable<string>
48
+ }) {
49
+ const stale: string[] = []
50
+ const keep = new Set([input.keep, ...Array.from(input.preserve ?? [])])
51
+ if (input.seen.has(input.keep)) input.seen.delete(input.keep)
52
+ input.seen.add(input.keep)
53
+ for (const id of input.seen) {
54
+ if (input.seen.size - stale.length <= input.limit) break
55
+ if (keep.has(id)) continue
56
+ stale.push(id)
57
+ }
58
+ for (const id of stale) {
59
+ input.seen.delete(id)
60
+ }
61
+ return stale
62
+ }
@@ -0,0 +1,25 @@
1
+ import type { RootLoadArgs } from "./types"
2
+
3
+ export async function loadRootSessionsWithFallback(input: RootLoadArgs) {
4
+ try {
5
+ const result = await input.list({ directory: input.directory, roots: true, limit: input.limit })
6
+ return {
7
+ data: result.data,
8
+ limit: input.limit,
9
+ limited: true,
10
+ } as const
11
+ } catch {
12
+ const result = await input.list({ directory: input.directory, roots: true })
13
+ return {
14
+ data: result.data,
15
+ limit: input.limit,
16
+ limited: false,
17
+ } as const
18
+ }
19
+ }
20
+
21
+ export function estimateRootSessionTotal(input: { count: number; limit: number; limited: boolean }) {
22
+ if (!input.limited) return input.count
23
+ if (input.count < input.limit) return input.count
24
+ return input.count + 1
25
+ }