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,552 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import type { Message, Part, PermissionRequest, Project, QuestionRequest, Session } from "@reign-labs/sdk/v2/client"
3
+ import { createStore } from "solid-js/store"
4
+ import type { State } from "./types"
5
+ import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } from "./event-reducer"
6
+
7
+ const rootSession = (input: { id: string; parentID?: string; archived?: number }) =>
8
+ ({
9
+ id: input.id,
10
+ parentID: input.parentID,
11
+ time: {
12
+ created: 1,
13
+ updated: 1,
14
+ archived: input.archived,
15
+ },
16
+ }) as Session
17
+
18
+ const userMessage = (id: string, sessionID: string) =>
19
+ ({
20
+ id,
21
+ sessionID,
22
+ role: "user",
23
+ time: { created: 1 },
24
+ agent: "assistant",
25
+ model: { providerID: "openai", modelID: "gpt" },
26
+ }) as Message
27
+
28
+ const textPart = (id: string, sessionID: string, messageID: string) =>
29
+ ({
30
+ id,
31
+ sessionID,
32
+ messageID,
33
+ type: "text",
34
+ text: id,
35
+ }) as Part
36
+
37
+ const permissionRequest = (id: string, sessionID: string, title = id) =>
38
+ ({
39
+ id,
40
+ sessionID,
41
+ permission: title,
42
+ patterns: ["*"],
43
+ metadata: {},
44
+ always: [],
45
+ }) as PermissionRequest
46
+
47
+ const questionRequest = (id: string, sessionID: string, title = id) =>
48
+ ({
49
+ id,
50
+ sessionID,
51
+ questions: [
52
+ {
53
+ question: title,
54
+ header: title,
55
+ options: [{ label: title, description: title }],
56
+ },
57
+ ],
58
+ }) as QuestionRequest
59
+
60
+ const baseState = (input: Partial<State> = {}) =>
61
+ ({
62
+ status: "complete",
63
+ agent: [],
64
+ command: [],
65
+ project: "",
66
+ projectMeta: undefined,
67
+ icon: undefined,
68
+ provider: {} as State["provider"],
69
+ config: {} as State["config"],
70
+ path: { directory: "/tmp" } as State["path"],
71
+ session: [],
72
+ sessionTotal: 0,
73
+ session_status: {},
74
+ session_diff: {},
75
+ todo: {},
76
+ permission: {},
77
+ question: {},
78
+ mcp: {},
79
+ lsp: [],
80
+ vcs: undefined,
81
+ limit: 10,
82
+ message: {},
83
+ part: {},
84
+ ...input,
85
+ }) as State
86
+
87
+ describe("applyGlobalEvent", () => {
88
+ test("upserts project.updated in sorted position", () => {
89
+ const project = [{ id: "a" }, { id: "c" }] as Project[]
90
+ let refreshCount = 0
91
+ applyGlobalEvent({
92
+ event: { type: "project.updated", properties: { id: "b" } },
93
+ project,
94
+ refresh: () => {
95
+ refreshCount += 1
96
+ },
97
+ setGlobalProject(next) {
98
+ if (typeof next === "function") next(project)
99
+ },
100
+ })
101
+
102
+ expect(project.map((x) => x.id)).toEqual(["a", "b", "c"])
103
+ expect(refreshCount).toBe(0)
104
+ })
105
+
106
+ test("handles global.disposed by triggering refresh", () => {
107
+ let refreshCount = 0
108
+ applyGlobalEvent({
109
+ event: { type: "global.disposed" },
110
+ project: [],
111
+ refresh: () => {
112
+ refreshCount += 1
113
+ },
114
+ setGlobalProject() {},
115
+ })
116
+
117
+ expect(refreshCount).toBe(1)
118
+ })
119
+
120
+ test("handles server.connected by triggering refresh", () => {
121
+ let refreshCount = 0
122
+ applyGlobalEvent({
123
+ event: { type: "server.connected" },
124
+ project: [],
125
+ refresh: () => {
126
+ refreshCount += 1
127
+ },
128
+ setGlobalProject() {},
129
+ })
130
+
131
+ expect(refreshCount).toBe(1)
132
+ })
133
+ })
134
+
135
+ describe("applyDirectoryEvent", () => {
136
+ test("inserts root sessions in sorted order and updates sessionTotal", () => {
137
+ const [store, setStore] = createStore(
138
+ baseState({
139
+ session: [rootSession({ id: "b" })],
140
+ sessionTotal: 1,
141
+ }),
142
+ )
143
+
144
+ applyDirectoryEvent({
145
+ event: { type: "session.created", properties: { info: rootSession({ id: "a" }) } },
146
+ store,
147
+ setStore,
148
+ push() {},
149
+ directory: "/tmp",
150
+ loadLsp() {},
151
+ })
152
+
153
+ expect(store.session.map((x) => x.id)).toEqual(["a", "b"])
154
+ expect(store.sessionTotal).toBe(2)
155
+
156
+ applyDirectoryEvent({
157
+ event: { type: "session.created", properties: { info: rootSession({ id: "c", parentID: "a" }) } },
158
+ store,
159
+ setStore,
160
+ push() {},
161
+ directory: "/tmp",
162
+ loadLsp() {},
163
+ })
164
+
165
+ expect(store.sessionTotal).toBe(2)
166
+ })
167
+
168
+ test("cleans session caches when archived", () => {
169
+ const message = userMessage("msg_1", "ses_1")
170
+ const [store, setStore] = createStore(
171
+ baseState({
172
+ session: [rootSession({ id: "ses_1" }), rootSession({ id: "ses_2" })],
173
+ sessionTotal: 2,
174
+ message: { ses_1: [message] },
175
+ part: { [message.id]: [textPart("prt_1", "ses_1", message.id)] },
176
+ session_diff: { ses_1: [] },
177
+ todo: { ses_1: [] },
178
+ permission: { ses_1: [] },
179
+ question: { ses_1: [] },
180
+ session_status: { ses_1: { type: "busy" } },
181
+ }),
182
+ )
183
+
184
+ applyDirectoryEvent({
185
+ event: { type: "session.updated", properties: { info: rootSession({ id: "ses_1", archived: 10 }) } },
186
+ store,
187
+ setStore,
188
+ push() {},
189
+ directory: "/tmp",
190
+ loadLsp() {},
191
+ })
192
+
193
+ expect(store.session.map((x) => x.id)).toEqual(["ses_2"])
194
+ expect(store.sessionTotal).toBe(1)
195
+ expect(store.message.ses_1).toBeUndefined()
196
+ expect(store.part[message.id]).toBeUndefined()
197
+ expect(store.session_diff.ses_1).toBeUndefined()
198
+ expect(store.todo.ses_1).toBeUndefined()
199
+ expect(store.permission.ses_1).toBeUndefined()
200
+ expect(store.question.ses_1).toBeUndefined()
201
+ expect(store.session_status.ses_1).toBeUndefined()
202
+ })
203
+
204
+ test("cleans session caches when deleted and decrements only root totals", () => {
205
+ const cases = [
206
+ { info: rootSession({ id: "ses_1" }), expectedTotal: 1 },
207
+ { info: rootSession({ id: "ses_2", parentID: "ses_1" }), expectedTotal: 2 },
208
+ ]
209
+
210
+ for (const item of cases) {
211
+ const message = userMessage("msg_1", item.info.id)
212
+ const [store, setStore] = createStore(
213
+ baseState({
214
+ session: [
215
+ rootSession({ id: "ses_1" }),
216
+ rootSession({ id: "ses_2", parentID: "ses_1" }),
217
+ rootSession({ id: "ses_3" }),
218
+ ],
219
+ sessionTotal: 2,
220
+ message: { [item.info.id]: [message] },
221
+ part: { [message.id]: [textPart("prt_1", item.info.id, message.id)] },
222
+ session_diff: { [item.info.id]: [] },
223
+ todo: { [item.info.id]: [] },
224
+ permission: { [item.info.id]: [] },
225
+ question: { [item.info.id]: [] },
226
+ session_status: { [item.info.id]: { type: "busy" } },
227
+ }),
228
+ )
229
+
230
+ applyDirectoryEvent({
231
+ event: { type: "session.deleted", properties: { info: item.info } },
232
+ store,
233
+ setStore,
234
+ push() {},
235
+ directory: "/tmp",
236
+ loadLsp() {},
237
+ })
238
+
239
+ expect(store.session.find((x) => x.id === item.info.id)).toBeUndefined()
240
+ expect(store.sessionTotal).toBe(item.expectedTotal)
241
+ expect(store.message[item.info.id]).toBeUndefined()
242
+ expect(store.part[message.id]).toBeUndefined()
243
+ expect(store.session_diff[item.info.id]).toBeUndefined()
244
+ expect(store.todo[item.info.id]).toBeUndefined()
245
+ expect(store.permission[item.info.id]).toBeUndefined()
246
+ expect(store.question[item.info.id]).toBeUndefined()
247
+ expect(store.session_status[item.info.id]).toBeUndefined()
248
+ }
249
+ })
250
+
251
+ test("cleans caches for trimmed sessions on session.created", () => {
252
+ const dropped = rootSession({ id: "ses_b" })
253
+ const kept = rootSession({ id: "ses_a" })
254
+ const message = userMessage("msg_1", dropped.id)
255
+ const todos: string[] = []
256
+ const [store, setStore] = createStore(
257
+ baseState({
258
+ limit: 1,
259
+ session: [dropped],
260
+ message: { [dropped.id]: [message] },
261
+ part: { [message.id]: [textPart("prt_1", dropped.id, message.id)] },
262
+ session_diff: { [dropped.id]: [] },
263
+ todo: { [dropped.id]: [] },
264
+ permission: { [dropped.id]: [] },
265
+ question: { [dropped.id]: [] },
266
+ session_status: { [dropped.id]: { type: "busy" } },
267
+ }),
268
+ )
269
+
270
+ applyDirectoryEvent({
271
+ event: { type: "session.created", properties: { info: kept } },
272
+ store,
273
+ setStore,
274
+ push() {},
275
+ directory: "/tmp",
276
+ loadLsp() {},
277
+ setSessionTodo(sessionID, value) {
278
+ if (value !== undefined) return
279
+ todos.push(sessionID)
280
+ },
281
+ })
282
+
283
+ expect(store.session.map((x) => x.id)).toEqual([kept.id])
284
+ expect(store.message[dropped.id]).toBeUndefined()
285
+ expect(store.part[message.id]).toBeUndefined()
286
+ expect(store.session_diff[dropped.id]).toBeUndefined()
287
+ expect(store.todo[dropped.id]).toBeUndefined()
288
+ expect(store.permission[dropped.id]).toBeUndefined()
289
+ expect(store.question[dropped.id]).toBeUndefined()
290
+ expect(store.session_status[dropped.id]).toBeUndefined()
291
+ expect(todos).toEqual([dropped.id])
292
+ })
293
+
294
+ test("cleanupDroppedSessionCaches clears part-only orphan state", () => {
295
+ const [store, setStore] = createStore(
296
+ baseState({
297
+ session: [rootSession({ id: "ses_keep" })],
298
+ part: { msg_1: [textPart("prt_1", "ses_drop", "msg_1")] },
299
+ }),
300
+ )
301
+
302
+ cleanupDroppedSessionCaches(store, setStore, store.session)
303
+
304
+ expect(store.part.msg_1).toBeUndefined()
305
+ })
306
+
307
+ test("upserts and removes messages while clearing orphaned parts", () => {
308
+ const sessionID = "ses_1"
309
+ const [store, setStore] = createStore(
310
+ baseState({
311
+ message: { [sessionID]: [userMessage("msg_1", sessionID), userMessage("msg_3", sessionID)] },
312
+ part: { msg_2: [textPart("prt_1", sessionID, "msg_2")] },
313
+ }),
314
+ )
315
+
316
+ applyDirectoryEvent({
317
+ event: { type: "message.updated", properties: { info: userMessage("msg_2", sessionID) } },
318
+ store,
319
+ setStore,
320
+ push() {},
321
+ directory: "/tmp",
322
+ loadLsp() {},
323
+ })
324
+
325
+ expect(store.message[sessionID]?.map((x) => x.id)).toEqual(["msg_1", "msg_2", "msg_3"])
326
+
327
+ applyDirectoryEvent({
328
+ event: {
329
+ type: "message.updated",
330
+ properties: {
331
+ info: {
332
+ ...userMessage("msg_2", sessionID),
333
+ role: "assistant",
334
+ } as Message,
335
+ },
336
+ },
337
+ store,
338
+ setStore,
339
+ push() {},
340
+ directory: "/tmp",
341
+ loadLsp() {},
342
+ })
343
+
344
+ expect(store.message[sessionID]?.find((x) => x.id === "msg_2")?.role).toBe("assistant")
345
+
346
+ applyDirectoryEvent({
347
+ event: { type: "message.removed", properties: { sessionID, messageID: "msg_2" } },
348
+ store,
349
+ setStore,
350
+ push() {},
351
+ directory: "/tmp",
352
+ loadLsp() {},
353
+ })
354
+
355
+ expect(store.message[sessionID]?.map((x) => x.id)).toEqual(["msg_1", "msg_3"])
356
+ expect(store.part.msg_2).toBeUndefined()
357
+ })
358
+
359
+ test("upserts and prunes message parts", () => {
360
+ const sessionID = "ses_1"
361
+ const messageID = "msg_1"
362
+ const [store, setStore] = createStore(
363
+ baseState({
364
+ part: { [messageID]: [textPart("prt_1", sessionID, messageID), textPart("prt_3", sessionID, messageID)] },
365
+ }),
366
+ )
367
+
368
+ applyDirectoryEvent({
369
+ event: { type: "message.part.updated", properties: { part: textPart("prt_2", sessionID, messageID) } },
370
+ store,
371
+ setStore,
372
+ push() {},
373
+ directory: "/tmp",
374
+ loadLsp() {},
375
+ })
376
+ expect(store.part[messageID]?.map((x) => x.id)).toEqual(["prt_1", "prt_2", "prt_3"])
377
+
378
+ applyDirectoryEvent({
379
+ event: {
380
+ type: "message.part.updated",
381
+ properties: {
382
+ part: {
383
+ ...textPart("prt_2", sessionID, messageID),
384
+ text: "changed",
385
+ } as Part,
386
+ },
387
+ },
388
+ store,
389
+ setStore,
390
+ push() {},
391
+ directory: "/tmp",
392
+ loadLsp() {},
393
+ })
394
+ const updated = store.part[messageID]?.find((x) => x.id === "prt_2")
395
+ expect(updated?.type).toBe("text")
396
+ if (updated?.type === "text") expect(updated.text).toBe("changed")
397
+
398
+ applyDirectoryEvent({
399
+ event: { type: "message.part.removed", properties: { messageID, partID: "prt_1" } },
400
+ store,
401
+ setStore,
402
+ push() {},
403
+ directory: "/tmp",
404
+ loadLsp() {},
405
+ })
406
+ applyDirectoryEvent({
407
+ event: { type: "message.part.removed", properties: { messageID, partID: "prt_2" } },
408
+ store,
409
+ setStore,
410
+ push() {},
411
+ directory: "/tmp",
412
+ loadLsp() {},
413
+ })
414
+ applyDirectoryEvent({
415
+ event: { type: "message.part.removed", properties: { messageID, partID: "prt_3" } },
416
+ store,
417
+ setStore,
418
+ push() {},
419
+ directory: "/tmp",
420
+ loadLsp() {},
421
+ })
422
+
423
+ expect(store.part[messageID]).toBeUndefined()
424
+ })
425
+
426
+ test("tracks permission and question request lifecycles", () => {
427
+ const sessionID = "ses_1"
428
+ const [store, setStore] = createStore(
429
+ baseState({
430
+ permission: { [sessionID]: [permissionRequest("perm_1", sessionID), permissionRequest("perm_3", sessionID)] },
431
+ question: { [sessionID]: [questionRequest("q_1", sessionID), questionRequest("q_3", sessionID)] },
432
+ }),
433
+ )
434
+
435
+ applyDirectoryEvent({
436
+ event: { type: "permission.asked", properties: permissionRequest("perm_2", sessionID) },
437
+ store,
438
+ setStore,
439
+ push() {},
440
+ directory: "/tmp",
441
+ loadLsp() {},
442
+ })
443
+ expect(store.permission[sessionID]?.map((x) => x.id)).toEqual(["perm_1", "perm_2", "perm_3"])
444
+
445
+ applyDirectoryEvent({
446
+ event: { type: "permission.asked", properties: permissionRequest("perm_2", sessionID, "updated") },
447
+ store,
448
+ setStore,
449
+ push() {},
450
+ directory: "/tmp",
451
+ loadLsp() {},
452
+ })
453
+ expect(store.permission[sessionID]?.find((x) => x.id === "perm_2")?.permission).toBe("updated")
454
+
455
+ applyDirectoryEvent({
456
+ event: { type: "permission.replied", properties: { sessionID, requestID: "perm_2" } },
457
+ store,
458
+ setStore,
459
+ push() {},
460
+ directory: "/tmp",
461
+ loadLsp() {},
462
+ })
463
+ expect(store.permission[sessionID]?.map((x) => x.id)).toEqual(["perm_1", "perm_3"])
464
+
465
+ applyDirectoryEvent({
466
+ event: { type: "question.asked", properties: questionRequest("q_2", sessionID) },
467
+ store,
468
+ setStore,
469
+ push() {},
470
+ directory: "/tmp",
471
+ loadLsp() {},
472
+ })
473
+ expect(store.question[sessionID]?.map((x) => x.id)).toEqual(["q_1", "q_2", "q_3"])
474
+
475
+ applyDirectoryEvent({
476
+ event: { type: "question.asked", properties: questionRequest("q_2", sessionID, "updated") },
477
+ store,
478
+ setStore,
479
+ push() {},
480
+ directory: "/tmp",
481
+ loadLsp() {},
482
+ })
483
+ expect(store.question[sessionID]?.find((x) => x.id === "q_2")?.questions[0]?.header).toBe("updated")
484
+
485
+ applyDirectoryEvent({
486
+ event: { type: "question.rejected", properties: { sessionID, requestID: "q_2" } },
487
+ store,
488
+ setStore,
489
+ push() {},
490
+ directory: "/tmp",
491
+ loadLsp() {},
492
+ })
493
+ expect(store.question[sessionID]?.map((x) => x.id)).toEqual(["q_1", "q_3"])
494
+ })
495
+
496
+ test("updates vcs branch in store and cache", () => {
497
+ const [store, setStore] = createStore(baseState())
498
+ const [cacheStore, setCacheStore] = createStore({ value: undefined as State["vcs"] })
499
+
500
+ applyDirectoryEvent({
501
+ event: { type: "vcs.branch.updated", properties: { branch: "feature/test" } },
502
+ store,
503
+ setStore,
504
+ push() {},
505
+ directory: "/tmp",
506
+ loadLsp() {},
507
+ vcsCache: {
508
+ store: cacheStore,
509
+ setStore: setCacheStore,
510
+ ready: () => true,
511
+ },
512
+ })
513
+
514
+ expect(store.vcs).toEqual({ branch: "feature/test" })
515
+ expect(cacheStore.value).toEqual({ branch: "feature/test" })
516
+ })
517
+
518
+ test("routes disposal and lsp events to side-effect handlers", () => {
519
+ const [store, setStore] = createStore(baseState())
520
+ const pushes: string[] = []
521
+ let lspLoads = 0
522
+
523
+ applyDirectoryEvent({
524
+ event: { type: "server.instance.disposed" },
525
+ store,
526
+ setStore,
527
+ push(directory) {
528
+ pushes.push(directory)
529
+ },
530
+ directory: "/tmp",
531
+ loadLsp() {
532
+ lspLoads += 1
533
+ },
534
+ })
535
+
536
+ applyDirectoryEvent({
537
+ event: { type: "lsp.updated" },
538
+ store,
539
+ setStore,
540
+ push(directory) {
541
+ pushes.push(directory)
542
+ },
543
+ directory: "/tmp",
544
+ loadLsp() {
545
+ lspLoads += 1
546
+ },
547
+ })
548
+
549
+ expect(pushes).toEqual(["/tmp"])
550
+ expect(lspLoads).toBe(1)
551
+ })
552
+ })