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,618 @@
1
+ import { batch, createMemo } from "solid-js"
2
+ import { createStore, produce, reconcile } from "solid-js/store"
3
+ import { Binary } from "@reign-labs/util/binary"
4
+ import { retry } from "@reign-labs/util/retry"
5
+ import { createSimpleContext } from "@reign-labs/ui/context"
6
+ import {
7
+ clearSessionPrefetch,
8
+ getSessionPrefetch,
9
+ getSessionPrefetchPromise,
10
+ setSessionPrefetch,
11
+ } from "./global-sync/session-prefetch"
12
+ import { useGlobalSync } from "./global-sync"
13
+ import { useSDK } from "./sdk"
14
+ import type { Message, Part } from "@reign-labs/sdk/v2/client"
15
+ import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache"
16
+
17
+ const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
18
+
19
+ function sortParts(parts: Part[]) {
20
+ return parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id))
21
+ }
22
+
23
+ function runInflight(map: Map<string, Promise<void>>, key: string, task: () => Promise<void>) {
24
+ const pending = map.get(key)
25
+ if (pending) return pending
26
+ const promise = task().finally(() => {
27
+ map.delete(key)
28
+ })
29
+ map.set(key, promise)
30
+ return promise
31
+ }
32
+
33
+ const keyFor = (directory: string, id: string) => `${directory}\n${id}`
34
+
35
+ const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
36
+
37
+ function merge<T extends { id: string }>(a: readonly T[], b: readonly T[]) {
38
+ const map = new Map(a.map((item) => [item.id, item] as const))
39
+ for (const item of b) map.set(item.id, item)
40
+ return [...map.values()].sort((x, y) => cmp(x.id, y.id))
41
+ }
42
+
43
+ type OptimisticStore = {
44
+ message: Record<string, Message[] | undefined>
45
+ part: Record<string, Part[] | undefined>
46
+ }
47
+
48
+ type OptimisticAddInput = {
49
+ sessionID: string
50
+ message: Message
51
+ parts: Part[]
52
+ }
53
+
54
+ type OptimisticRemoveInput = {
55
+ sessionID: string
56
+ messageID: string
57
+ }
58
+
59
+ type OptimisticItem = {
60
+ message: Message
61
+ parts: Part[]
62
+ }
63
+
64
+ type MessagePage = {
65
+ session: Message[]
66
+ part: { id: string; part: Part[] }[]
67
+ cursor?: string
68
+ complete: boolean
69
+ }
70
+
71
+ const hasParts = (parts: Part[] | undefined, want: Part[]) => {
72
+ if (!parts) return want.length === 0
73
+ return want.every((part) => Binary.search(parts, part.id, (item) => item.id).found)
74
+ }
75
+
76
+ const mergeParts = (parts: Part[] | undefined, want: Part[]) => {
77
+ if (!parts) return sortParts(want)
78
+ const next = [...parts]
79
+ let changed = false
80
+ for (const part of want) {
81
+ const result = Binary.search(next, part.id, (item) => item.id)
82
+ if (result.found) continue
83
+ next.splice(result.index, 0, part)
84
+ changed = true
85
+ }
86
+ if (!changed) return parts
87
+ return next
88
+ }
89
+
90
+ export function mergeOptimisticPage(page: MessagePage, items: OptimisticItem[]) {
91
+ if (items.length === 0) return { ...page, confirmed: [] as string[] }
92
+
93
+ const session = [...page.session]
94
+ const part = new Map(page.part.map((item) => [item.id, sortParts(item.part)]))
95
+ const confirmed: string[] = []
96
+
97
+ for (const item of items) {
98
+ const result = Binary.search(session, item.message.id, (message) => message.id)
99
+ const found = result.found
100
+ if (!found) session.splice(result.index, 0, item.message)
101
+
102
+ const current = part.get(item.message.id)
103
+ if (found && hasParts(current, item.parts)) {
104
+ confirmed.push(item.message.id)
105
+ continue
106
+ }
107
+
108
+ part.set(item.message.id, mergeParts(current, item.parts))
109
+ }
110
+
111
+ return {
112
+ cursor: page.cursor,
113
+ complete: page.complete,
114
+ session,
115
+ part: [...part.entries()].sort((a, b) => cmp(a[0], b[0])).map(([id, part]) => ({ id, part })),
116
+ confirmed,
117
+ }
118
+ }
119
+
120
+ export function applyOptimisticAdd(draft: OptimisticStore, input: OptimisticAddInput) {
121
+ const messages = draft.message[input.sessionID]
122
+ if (messages) {
123
+ const result = Binary.search(messages, input.message.id, (m) => m.id)
124
+ messages.splice(result.index, 0, input.message)
125
+ } else {
126
+ draft.message[input.sessionID] = [input.message]
127
+ }
128
+ draft.part[input.message.id] = sortParts(input.parts)
129
+ }
130
+
131
+ export function applyOptimisticRemove(draft: OptimisticStore, input: OptimisticRemoveInput) {
132
+ const messages = draft.message[input.sessionID]
133
+ if (messages) {
134
+ const result = Binary.search(messages, input.messageID, (m) => m.id)
135
+ if (result.found) messages.splice(result.index, 1)
136
+ }
137
+ delete draft.part[input.messageID]
138
+ }
139
+
140
+ function setOptimisticAdd(setStore: (...args: unknown[]) => void, input: OptimisticAddInput) {
141
+ setStore("message", input.sessionID, (messages: Message[] | undefined) => {
142
+ if (!messages) return [input.message]
143
+ const result = Binary.search(messages, input.message.id, (m) => m.id)
144
+ const next = [...messages]
145
+ next.splice(result.index, 0, input.message)
146
+ return next
147
+ })
148
+ setStore("part", input.message.id, sortParts(input.parts))
149
+ }
150
+
151
+ function setOptimisticRemove(setStore: (...args: unknown[]) => void, input: OptimisticRemoveInput) {
152
+ setStore("message", input.sessionID, (messages: Message[] | undefined) => {
153
+ if (!messages) return messages
154
+ const result = Binary.search(messages, input.messageID, (m) => m.id)
155
+ if (!result.found) return messages
156
+ const next = [...messages]
157
+ next.splice(result.index, 1)
158
+ return next
159
+ })
160
+ setStore("part", (part: Record<string, Part[] | undefined>) => {
161
+ if (!(input.messageID in part)) return part
162
+ const next = { ...part }
163
+ delete next[input.messageID]
164
+ return next
165
+ })
166
+ }
167
+
168
+ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
169
+ name: "Sync",
170
+ init: () => {
171
+ const globalSync = useGlobalSync()
172
+ const sdk = useSDK()
173
+
174
+ type Child = ReturnType<(typeof globalSync)["child"]>
175
+ type Setter = Child[1]
176
+
177
+ const current = createMemo(() => globalSync.child(sdk.directory))
178
+ const target = (directory?: string) => {
179
+ if (!directory || directory === sdk.directory) return current()
180
+ return globalSync.child(directory)
181
+ }
182
+ const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
183
+ const messagePageSize = 200
184
+ const inflight = new Map<string, Promise<void>>()
185
+ const inflightDiff = new Map<string, Promise<void>>()
186
+ const inflightTodo = new Map<string, Promise<void>>()
187
+ const optimistic = new Map<string, Map<string, OptimisticItem>>()
188
+ const maxDirs = 30
189
+ const seen = new Map<string, Set<string>>()
190
+ const [meta, setMeta] = createStore({
191
+ limit: {} as Record<string, number>,
192
+ cursor: {} as Record<string, string | undefined>,
193
+ complete: {} as Record<string, boolean>,
194
+ loading: {} as Record<string, boolean>,
195
+ })
196
+
197
+ const getSession = (sessionID: string) => {
198
+ const store = current()[0]
199
+ const match = Binary.search(store.session, sessionID, (s) => s.id)
200
+ if (match.found) return store.session[match.index]
201
+ return undefined
202
+ }
203
+
204
+ const setOptimistic = (directory: string, sessionID: string, item: OptimisticItem) => {
205
+ const key = keyFor(directory, sessionID)
206
+ const list = optimistic.get(key)
207
+ if (list) {
208
+ list.set(item.message.id, { message: item.message, parts: sortParts(item.parts) })
209
+ return
210
+ }
211
+ optimistic.set(key, new Map([[item.message.id, { message: item.message, parts: sortParts(item.parts) }]]))
212
+ }
213
+
214
+ const clearOptimistic = (directory: string, sessionID: string, messageID?: string) => {
215
+ const key = keyFor(directory, sessionID)
216
+ if (!messageID) {
217
+ optimistic.delete(key)
218
+ return
219
+ }
220
+
221
+ const list = optimistic.get(key)
222
+ if (!list) return
223
+ list.delete(messageID)
224
+ if (list.size === 0) optimistic.delete(key)
225
+ }
226
+
227
+ const getOptimistic = (directory: string, sessionID: string) => [
228
+ ...(optimistic.get(keyFor(directory, sessionID))?.values() ?? []),
229
+ ]
230
+
231
+ const seenFor = (directory: string) => {
232
+ const existing = seen.get(directory)
233
+ if (existing) {
234
+ seen.delete(directory)
235
+ seen.set(directory, existing)
236
+ return existing
237
+ }
238
+ const created = new Set<string>()
239
+ seen.set(directory, created)
240
+ while (seen.size > maxDirs) {
241
+ const first = seen.keys().next().value
242
+ if (!first) break
243
+ const stale = [...(seen.get(first) ?? [])]
244
+ seen.delete(first)
245
+ const [, setStore] = globalSync.child(first, { bootstrap: false })
246
+ evict(first, setStore, stale)
247
+ }
248
+ return created
249
+ }
250
+
251
+ const clearMeta = (directory: string, sessionIDs: string[]) => {
252
+ if (sessionIDs.length === 0) return
253
+ for (const sessionID of sessionIDs) {
254
+ clearOptimistic(directory, sessionID)
255
+ }
256
+ setMeta(
257
+ produce((draft) => {
258
+ for (const sessionID of sessionIDs) {
259
+ const key = keyFor(directory, sessionID)
260
+ delete draft.limit[key]
261
+ delete draft.cursor[key]
262
+ delete draft.complete[key]
263
+ delete draft.loading[key]
264
+ }
265
+ }),
266
+ )
267
+ }
268
+
269
+ const evict = (directory: string, setStore: Setter, sessionIDs: string[]) => {
270
+ if (sessionIDs.length === 0) return
271
+ clearSessionPrefetch(directory, sessionIDs)
272
+ for (const sessionID of sessionIDs) {
273
+ globalSync.todo.set(sessionID, undefined)
274
+ }
275
+ setStore(
276
+ produce((draft) => {
277
+ dropSessionCaches(draft, sessionIDs)
278
+ }),
279
+ )
280
+ clearMeta(directory, sessionIDs)
281
+ }
282
+
283
+ const touch = (directory: string, setStore: Setter, sessionID: string) => {
284
+ const stale = pickSessionCacheEvictions({
285
+ seen: seenFor(directory),
286
+ keep: sessionID,
287
+ limit: SESSION_CACHE_LIMIT,
288
+ })
289
+ evict(directory, setStore, stale)
290
+ }
291
+
292
+ const fetchMessages = async (input: {
293
+ client: typeof sdk.client
294
+ sessionID: string
295
+ limit: number
296
+ before?: string
297
+ }) => {
298
+ const messages = await retry(() =>
299
+ input.client.session.messages({ sessionID: input.sessionID, limit: input.limit, before: input.before }),
300
+ )
301
+ const items = (messages.data ?? []).filter((x) => !!x?.info?.id)
302
+ const session = items.map((x) => x.info).sort((a, b) => cmp(a.id, b.id))
303
+ const part = items.map((message) => ({ id: message.info.id, part: sortParts(message.parts) }))
304
+ const cursor = messages.response.headers.get("x-next-cursor") ?? undefined
305
+ return {
306
+ session,
307
+ part,
308
+ cursor,
309
+ complete: !cursor,
310
+ }
311
+ }
312
+
313
+ const tracked = (directory: string, sessionID: string) => seen.get(directory)?.has(sessionID) ?? false
314
+
315
+ const loadMessages = async (input: {
316
+ directory: string
317
+ client: typeof sdk.client
318
+ setStore: Setter
319
+ sessionID: string
320
+ limit: number
321
+ before?: string
322
+ mode?: "replace" | "prepend"
323
+ }) => {
324
+ const key = keyFor(input.directory, input.sessionID)
325
+ if (meta.loading[key]) return
326
+
327
+ setMeta("loading", key, true)
328
+ await fetchMessages(input)
329
+ .then((page) => {
330
+ if (!tracked(input.directory, input.sessionID)) return
331
+ const next = mergeOptimisticPage(page, getOptimistic(input.directory, input.sessionID))
332
+ for (const messageID of next.confirmed) {
333
+ clearOptimistic(input.directory, input.sessionID, messageID)
334
+ }
335
+ const [store] = globalSync.child(input.directory, { bootstrap: false })
336
+ const cached = input.mode === "prepend" ? (store.message[input.sessionID] ?? []) : []
337
+ const message = input.mode === "prepend" ? merge(cached, next.session) : next.session
338
+ batch(() => {
339
+ input.setStore("message", input.sessionID, reconcile(message, { key: "id" }))
340
+ for (const p of next.part) {
341
+ const filtered = p.part.filter((x) => !SKIP_PARTS.has(x.type))
342
+ if (filtered.length) input.setStore("part", p.id, filtered)
343
+ }
344
+ setMeta("limit", key, message.length)
345
+ setMeta("cursor", key, next.cursor)
346
+ setMeta("complete", key, next.complete)
347
+ setSessionPrefetch({
348
+ directory: input.directory,
349
+ sessionID: input.sessionID,
350
+ limit: message.length,
351
+ cursor: next.cursor,
352
+ complete: next.complete,
353
+ })
354
+ })
355
+ })
356
+ .finally(() => {
357
+ setMeta(
358
+ produce((draft) => {
359
+ if (!tracked(input.directory, input.sessionID)) {
360
+ delete draft.loading[key]
361
+ return
362
+ }
363
+ draft.loading[key] = false
364
+ }),
365
+ )
366
+ })
367
+ }
368
+
369
+ return {
370
+ get data() {
371
+ return current()[0]
372
+ },
373
+ get set(): Setter {
374
+ return current()[1]
375
+ },
376
+ get status() {
377
+ return current()[0].status
378
+ },
379
+ get ready() {
380
+ return current()[0].status !== "loading"
381
+ },
382
+ get project() {
383
+ const store = current()[0]
384
+ const match = Binary.search(globalSync.data.project, store.project, (p) => p.id)
385
+ if (match.found) return globalSync.data.project[match.index]
386
+ return undefined
387
+ },
388
+ session: {
389
+ get: getSession,
390
+ optimistic: {
391
+ add(input: { directory?: string; sessionID: string; message: Message; parts: Part[] }) {
392
+ const directory = input.directory ?? sdk.directory
393
+ const [, setStore] = target(input.directory)
394
+ setOptimistic(directory, input.sessionID, { message: input.message, parts: input.parts })
395
+ setOptimisticAdd(setStore as (...args: unknown[]) => void, input)
396
+ },
397
+ remove(input: { directory?: string; sessionID: string; messageID: string }) {
398
+ const directory = input.directory ?? sdk.directory
399
+ const [, setStore] = target(input.directory)
400
+ clearOptimistic(directory, input.sessionID, input.messageID)
401
+ setOptimisticRemove(setStore as (...args: unknown[]) => void, input)
402
+ },
403
+ },
404
+ addOptimisticMessage(input: {
405
+ sessionID: string
406
+ messageID: string
407
+ parts: Part[]
408
+ agent: string
409
+ model: { providerID: string; modelID: string }
410
+ variant?: string
411
+ }) {
412
+ const message: Message = {
413
+ id: input.messageID,
414
+ sessionID: input.sessionID,
415
+ role: "user",
416
+ time: { created: Date.now() },
417
+ agent: input.agent,
418
+ model: input.model,
419
+ variant: input.variant,
420
+ }
421
+ const [, setStore] = target()
422
+ setOptimistic(sdk.directory, input.sessionID, { message, parts: input.parts })
423
+ setOptimisticAdd(setStore as (...args: unknown[]) => void, {
424
+ sessionID: input.sessionID,
425
+ message,
426
+ parts: input.parts,
427
+ })
428
+ },
429
+ async sync(sessionID: string, opts?: { force?: boolean }) {
430
+ const directory = sdk.directory
431
+ const client = sdk.client
432
+ const [store, setStore] = globalSync.child(directory)
433
+ const key = keyFor(directory, sessionID)
434
+
435
+ touch(directory, setStore, sessionID)
436
+
437
+ const seeded = getSessionPrefetch(directory, sessionID)
438
+ if (seeded && store.message[sessionID] !== undefined && meta.limit[key] === undefined) {
439
+ batch(() => {
440
+ setMeta("limit", key, seeded.limit)
441
+ setMeta("cursor", key, seeded.cursor)
442
+ setMeta("complete", key, seeded.complete)
443
+ setMeta("loading", key, false)
444
+ })
445
+ }
446
+
447
+ return runInflight(inflight, key, async () => {
448
+ const pending = getSessionPrefetchPromise(directory, sessionID)
449
+ if (pending) {
450
+ await pending
451
+ const seeded = getSessionPrefetch(directory, sessionID)
452
+ if (seeded && store.message[sessionID] !== undefined && meta.limit[key] === undefined) {
453
+ batch(() => {
454
+ setMeta("limit", key, seeded.limit)
455
+ setMeta("cursor", key, seeded.cursor)
456
+ setMeta("complete", key, seeded.complete)
457
+ setMeta("loading", key, false)
458
+ })
459
+ }
460
+ }
461
+
462
+ const hasSession = Binary.search(store.session, sessionID, (s) => s.id).found
463
+ const cached = store.message[sessionID] !== undefined && meta.limit[key] !== undefined
464
+ if (cached && hasSession && !opts?.force) return
465
+
466
+ const limit = meta.limit[key] ?? messagePageSize
467
+ const sessionReq =
468
+ hasSession && !opts?.force
469
+ ? Promise.resolve()
470
+ : retry(() => client.session.get({ sessionID })).then((session) => {
471
+ if (!tracked(directory, sessionID)) return
472
+ const data = session.data
473
+ if (!data) return
474
+ setStore(
475
+ "session",
476
+ produce((draft) => {
477
+ const match = Binary.search(draft, sessionID, (s) => s.id)
478
+ if (match.found) {
479
+ draft[match.index] = data
480
+ return
481
+ }
482
+ draft.splice(match.index, 0, data)
483
+ }),
484
+ )
485
+ })
486
+
487
+ const messagesReq =
488
+ cached && !opts?.force
489
+ ? Promise.resolve()
490
+ : loadMessages({
491
+ directory,
492
+ client,
493
+ setStore,
494
+ sessionID,
495
+ limit,
496
+ })
497
+
498
+ await Promise.all([sessionReq, messagesReq])
499
+ })
500
+ },
501
+ async diff(sessionID: string, opts?: { force?: boolean }) {
502
+ const directory = sdk.directory
503
+ const client = sdk.client
504
+ const [store, setStore] = globalSync.child(directory)
505
+ touch(directory, setStore, sessionID)
506
+ if (store.session_diff[sessionID] !== undefined && !opts?.force) return
507
+
508
+ const key = keyFor(directory, sessionID)
509
+ return runInflight(inflightDiff, key, () =>
510
+ retry(() => client.session.diff({ sessionID })).then((diff) => {
511
+ if (!tracked(directory, sessionID)) return
512
+ setStore("session_diff", sessionID, reconcile(diff.data ?? [], { key: "file" }))
513
+ }),
514
+ )
515
+ },
516
+ async todo(sessionID: string, opts?: { force?: boolean }) {
517
+ const directory = sdk.directory
518
+ const client = sdk.client
519
+ const [store, setStore] = globalSync.child(directory)
520
+ touch(directory, setStore, sessionID)
521
+ const existing = store.todo[sessionID]
522
+ const cached = globalSync.data.session_todo[sessionID]
523
+ if (existing !== undefined) {
524
+ if (cached === undefined) {
525
+ globalSync.todo.set(sessionID, existing)
526
+ }
527
+ if (!opts?.force) return
528
+ }
529
+
530
+ if (cached !== undefined) {
531
+ setStore("todo", sessionID, reconcile(cached, { key: "id" }))
532
+ }
533
+
534
+ const key = keyFor(directory, sessionID)
535
+ return runInflight(inflightTodo, key, () =>
536
+ retry(() => client.session.todo({ sessionID })).then((todo) => {
537
+ if (!tracked(directory, sessionID)) return
538
+ const list = todo.data ?? []
539
+ setStore("todo", sessionID, reconcile(list, { key: "id" }))
540
+ globalSync.todo.set(sessionID, list)
541
+ }),
542
+ )
543
+ },
544
+ history: {
545
+ more(sessionID: string) {
546
+ const store = current()[0]
547
+ const key = keyFor(sdk.directory, sessionID)
548
+ if (store.message[sessionID] === undefined) return false
549
+ if (meta.limit[key] === undefined) return false
550
+ if (meta.complete[key]) return false
551
+ return !!meta.cursor[key]
552
+ },
553
+ loading(sessionID: string) {
554
+ const key = keyFor(sdk.directory, sessionID)
555
+ return meta.loading[key] ?? false
556
+ },
557
+ async loadMore(sessionID: string, count?: number) {
558
+ const directory = sdk.directory
559
+ const client = sdk.client
560
+ const [, setStore] = globalSync.child(directory)
561
+ touch(directory, setStore, sessionID)
562
+ const key = keyFor(directory, sessionID)
563
+ const step = count ?? messagePageSize
564
+ if (meta.loading[key]) return
565
+ if (meta.complete[key]) return
566
+ const before = meta.cursor[key]
567
+ if (!before) return
568
+
569
+ await loadMessages({
570
+ directory,
571
+ client,
572
+ setStore,
573
+ sessionID,
574
+ limit: step,
575
+ before,
576
+ mode: "prepend",
577
+ })
578
+ },
579
+ },
580
+ evict(sessionID: string, directory = sdk.directory) {
581
+ const [, setStore] = globalSync.child(directory)
582
+ seenFor(directory).delete(sessionID)
583
+ evict(directory, setStore, [sessionID])
584
+ },
585
+ fetch: async (count = 10) => {
586
+ const directory = sdk.directory
587
+ const client = sdk.client
588
+ const [store, setStore] = globalSync.child(directory)
589
+ setStore("limit", (x) => x + count)
590
+ await client.session.list().then((x) => {
591
+ const sessions = (x.data ?? [])
592
+ .filter((s) => !!s?.id)
593
+ .sort((a, b) => cmp(a.id, b.id))
594
+ .slice(0, store.limit)
595
+ setStore("session", reconcile(sessions, { key: "id" }))
596
+ })
597
+ },
598
+ more: createMemo(() => current()[0].session.length >= current()[0].limit),
599
+ archive: async (sessionID: string) => {
600
+ const directory = sdk.directory
601
+ const client = sdk.client
602
+ const [, setStore] = globalSync.child(directory)
603
+ await client.session.update({ sessionID, time: { archived: Date.now() } })
604
+ setStore(
605
+ produce((draft) => {
606
+ const match = Binary.search(draft.session, sessionID, (s) => s.id)
607
+ if (match.found) draft.session.splice(match.index, 1)
608
+ }),
609
+ )
610
+ },
611
+ },
612
+ absolute,
613
+ get directory() {
614
+ return current()[0].path.directory
615
+ },
616
+ }
617
+ },
618
+ })
@@ -0,0 +1,51 @@
1
+ import { dict as ar } from "@/i18n/ar"
2
+ import { dict as br } from "@/i18n/br"
3
+ import { dict as bs } from "@/i18n/bs"
4
+ import { dict as da } from "@/i18n/da"
5
+ import { dict as de } from "@/i18n/de"
6
+ import { dict as en } from "@/i18n/en"
7
+ import { dict as es } from "@/i18n/es"
8
+ import { dict as fr } from "@/i18n/fr"
9
+ import { dict as ja } from "@/i18n/ja"
10
+ import { dict as ko } from "@/i18n/ko"
11
+ import { dict as no } from "@/i18n/no"
12
+ import { dict as pl } from "@/i18n/pl"
13
+ import { dict as ru } from "@/i18n/ru"
14
+ import { dict as th } from "@/i18n/th"
15
+ import { dict as tr } from "@/i18n/tr"
16
+ import { dict as zh } from "@/i18n/zh"
17
+ import { dict as zht } from "@/i18n/zht"
18
+
19
+ const numbered = Array.from(
20
+ new Set([
21
+ en["terminal.title.numbered"],
22
+ ar["terminal.title.numbered"],
23
+ br["terminal.title.numbered"],
24
+ bs["terminal.title.numbered"],
25
+ da["terminal.title.numbered"],
26
+ de["terminal.title.numbered"],
27
+ es["terminal.title.numbered"],
28
+ fr["terminal.title.numbered"],
29
+ ja["terminal.title.numbered"],
30
+ ko["terminal.title.numbered"],
31
+ no["terminal.title.numbered"],
32
+ pl["terminal.title.numbered"],
33
+ ru["terminal.title.numbered"],
34
+ th["terminal.title.numbered"],
35
+ tr["terminal.title.numbered"],
36
+ zh["terminal.title.numbered"],
37
+ zht["terminal.title.numbered"],
38
+ ]),
39
+ )
40
+
41
+ export function defaultTitle(number: number) {
42
+ return en["terminal.title.numbered"].replace("{{number}}", String(number))
43
+ }
44
+
45
+ export function isDefaultTitle(title: string, number: number) {
46
+ return numbered.some((text) => title === text.replace("{{number}}", String(number)))
47
+ }
48
+
49
+ export function titleNumber(title: string, max: number) {
50
+ return Array.from({ length: max }, (_, idx) => idx + 1).find((number) => isDefaultTitle(title, number))
51
+ }