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,408 @@
1
+ import type {
2
+ Config,
3
+ OpencodeClient,
4
+ Path,
5
+ Project,
6
+ ProviderAuthResponse,
7
+ ProviderListResponse,
8
+ Todo,
9
+ } from "@reign-labs/sdk/v2/client"
10
+ import { showToast } from "@reign-labs/ui/toast"
11
+ import { getFilename } from "@reign-labs/util/path"
12
+ import {
13
+ createContext,
14
+ getOwner,
15
+ Match,
16
+ onCleanup,
17
+ onMount,
18
+ type ParentProps,
19
+ Switch,
20
+ untrack,
21
+ useContext,
22
+ } from "solid-js"
23
+ import { createStore, produce, reconcile } from "solid-js/store"
24
+ import { useLanguage } from "@/context/language"
25
+ import { Persist, persisted } from "@/utils/persist"
26
+ import type { InitError } from "../pages/error"
27
+ import { useGlobalSDK } from "./global-sdk"
28
+ import { bootstrapDirectory, bootstrapGlobal } from "./global-sync/bootstrap"
29
+ import { createChildStoreManager } from "./global-sync/child-store"
30
+ import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } from "./global-sync/event-reducer"
31
+ import { createRefreshQueue } from "./global-sync/queue"
32
+ import { clearSessionPrefetchDirectory } from "./global-sync/session-prefetch"
33
+ import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global-sync/session-load"
34
+ import { trimSessions } from "./global-sync/session-trim"
35
+ import type { ProjectMeta } from "./global-sync/types"
36
+ import { SESSION_RECENT_LIMIT } from "./global-sync/types"
37
+ import { sanitizeProject } from "./global-sync/utils"
38
+ import { formatServerError } from "@/utils/server-errors"
39
+
40
+ type GlobalStore = {
41
+ ready: boolean
42
+ error?: InitError
43
+ path: Path
44
+ project: Project[]
45
+ session_todo: {
46
+ [sessionID: string]: Todo[]
47
+ }
48
+ provider: ProviderListResponse
49
+ provider_auth: ProviderAuthResponse
50
+ config: Config
51
+ reload: undefined | "pending" | "complete"
52
+ }
53
+
54
+ function createGlobalSync() {
55
+ const globalSDK = useGlobalSDK()
56
+ const language = useLanguage()
57
+ const owner = getOwner()
58
+ if (!owner) throw new Error("GlobalSync must be created within owner")
59
+
60
+ const sdkCache = new Map<string, OpencodeClient>()
61
+ const booting = new Map<string, Promise<void>>()
62
+ const sessionLoads = new Map<string, Promise<void>>()
63
+ const sessionMeta = new Map<string, { limit: number }>()
64
+
65
+ const [projectCache, setProjectCache, projectInit] = persisted(
66
+ Persist.global("globalSync.project", ["globalSync.project.v1"]),
67
+ createStore({ value: [] as Project[] }),
68
+ )
69
+
70
+ const [globalStore, setGlobalStore] = createStore<GlobalStore>({
71
+ ready: false,
72
+ path: { state: "", config: "", worktree: "", directory: "", home: "" },
73
+ project: projectCache.value,
74
+ session_todo: {},
75
+ provider: { all: [], connected: [], default: {} },
76
+ provider_auth: {},
77
+ config: {},
78
+ reload: undefined,
79
+ })
80
+
81
+ let active = true
82
+ let projectWritten = false
83
+
84
+ onCleanup(() => {
85
+ active = false
86
+ })
87
+
88
+ const cacheProjects = () => {
89
+ setProjectCache(
90
+ "value",
91
+ untrack(() => globalStore.project.map(sanitizeProject)),
92
+ )
93
+ }
94
+
95
+ const setProjects = (next: Project[] | ((draft: Project[]) => void)) => {
96
+ projectWritten = true
97
+ if (typeof next === "function") {
98
+ setGlobalStore("project", produce(next))
99
+ cacheProjects()
100
+ return
101
+ }
102
+ setGlobalStore("project", next)
103
+ cacheProjects()
104
+ }
105
+
106
+ const setBootStore = ((...input: unknown[]) => {
107
+ if (input[0] === "project" && Array.isArray(input[1])) {
108
+ setProjects(input[1] as Project[])
109
+ return input[1]
110
+ }
111
+ return (setGlobalStore as (...args: unknown[]) => unknown)(...input)
112
+ }) as typeof setGlobalStore
113
+
114
+ const set = ((...input: unknown[]) => {
115
+ if (input[0] === "project" && (Array.isArray(input[1]) || typeof input[1] === "function")) {
116
+ setProjects(input[1] as Project[] | ((draft: Project[]) => void))
117
+ return input[1]
118
+ }
119
+ return (setGlobalStore as (...args: unknown[]) => unknown)(...input)
120
+ }) as typeof setGlobalStore
121
+
122
+ if (projectInit instanceof Promise) {
123
+ void projectInit.then(() => {
124
+ if (!active) return
125
+ if (projectWritten) return
126
+ const cached = projectCache.value
127
+ if (cached.length === 0) return
128
+ setGlobalStore("project", cached)
129
+ })
130
+ }
131
+
132
+ const setSessionTodo = (sessionID: string, todos: Todo[] | undefined) => {
133
+ if (!sessionID) return
134
+ if (!todos) {
135
+ setGlobalStore(
136
+ "session_todo",
137
+ produce((draft) => {
138
+ delete draft[sessionID]
139
+ }),
140
+ )
141
+ return
142
+ }
143
+ setGlobalStore("session_todo", sessionID, reconcile(todos, { key: "id" }))
144
+ }
145
+
146
+ const paused = () => untrack(() => globalStore.reload) !== undefined
147
+
148
+ const queue = createRefreshQueue({
149
+ paused,
150
+ bootstrap,
151
+ bootstrapInstance,
152
+ })
153
+
154
+ const children = createChildStoreManager({
155
+ owner,
156
+ isBooting: (directory) => booting.has(directory),
157
+ isLoadingSessions: (directory) => sessionLoads.has(directory),
158
+ onBootstrap: (directory) => {
159
+ void bootstrapInstance(directory)
160
+ },
161
+ onDispose: (directory) => {
162
+ queue.clear(directory)
163
+ sessionMeta.delete(directory)
164
+ sdkCache.delete(directory)
165
+ clearSessionPrefetchDirectory(directory)
166
+ },
167
+ translate: language.t,
168
+ })
169
+
170
+ const sdkFor = (directory: string) => {
171
+ const cached = sdkCache.get(directory)
172
+ if (cached) return cached
173
+ const sdk = globalSDK.createClient({
174
+ directory,
175
+ throwOnError: true,
176
+ })
177
+ sdkCache.set(directory, sdk)
178
+ return sdk
179
+ }
180
+
181
+ async function loadSessions(directory: string) {
182
+ const pending = sessionLoads.get(directory)
183
+ if (pending) return pending
184
+
185
+ children.pin(directory)
186
+ const [store, setStore] = children.child(directory, { bootstrap: false })
187
+ const meta = sessionMeta.get(directory)
188
+ if (meta && meta.limit >= store.limit) {
189
+ const next = trimSessions(store.session, {
190
+ limit: store.limit,
191
+ permission: store.permission,
192
+ })
193
+ if (next.length !== store.session.length) {
194
+ setStore("session", reconcile(next, { key: "id" }))
195
+ cleanupDroppedSessionCaches(store, setStore, next, setSessionTodo)
196
+ }
197
+ children.unpin(directory)
198
+ return
199
+ }
200
+
201
+ const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT)
202
+ const promise = loadRootSessionsWithFallback({
203
+ directory,
204
+ limit,
205
+ list: (query) => globalSDK.client.session.list(query),
206
+ })
207
+ .then((x) => {
208
+ const nonArchived = (x.data ?? [])
209
+ .filter((s) => !!s?.id)
210
+ .filter((s) => !s.time?.archived)
211
+ .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
212
+ const limit = store.limit
213
+ const childSessions = store.session.filter((s) => !!s.parentID)
214
+ const sessions = trimSessions([...nonArchived, ...childSessions], {
215
+ limit,
216
+ permission: store.permission,
217
+ })
218
+ setStore(
219
+ "sessionTotal",
220
+ estimateRootSessionTotal({
221
+ count: nonArchived.length,
222
+ limit: x.limit,
223
+ limited: x.limited,
224
+ }),
225
+ )
226
+ setStore("session", reconcile(sessions, { key: "id" }))
227
+ cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
228
+ sessionMeta.set(directory, { limit })
229
+ })
230
+ .catch((err) => {
231
+ console.error("Failed to load sessions", err)
232
+ const project = getFilename(directory)
233
+ showToast({
234
+ variant: "error",
235
+ title: language.t("toast.session.listFailed.title", { project }),
236
+ description: formatServerError(err, language.t),
237
+ })
238
+ })
239
+
240
+ sessionLoads.set(directory, promise)
241
+ promise.finally(() => {
242
+ sessionLoads.delete(directory)
243
+ children.unpin(directory)
244
+ })
245
+ return promise
246
+ }
247
+
248
+ async function bootstrapInstance(directory: string) {
249
+ if (!directory) return
250
+ const pending = booting.get(directory)
251
+ if (pending) return pending
252
+
253
+ children.pin(directory)
254
+ const promise = (async () => {
255
+ const child = children.ensureChild(directory)
256
+ const cache = children.vcsCache.get(directory)
257
+ if (!cache) return
258
+ const sdk = sdkFor(directory)
259
+ await bootstrapDirectory({
260
+ directory,
261
+ sdk,
262
+ store: child[0],
263
+ setStore: child[1],
264
+ vcsCache: cache,
265
+ loadSessions,
266
+ translate: language.t,
267
+ })
268
+ })()
269
+
270
+ booting.set(directory, promise)
271
+ promise.finally(() => {
272
+ booting.delete(directory)
273
+ children.unpin(directory)
274
+ })
275
+ return promise
276
+ }
277
+
278
+ const unsub = globalSDK.event.listen((e) => {
279
+ const directory = e.name
280
+ const event = e.details
281
+
282
+ if (directory === "global") {
283
+ applyGlobalEvent({
284
+ event,
285
+ project: globalStore.project,
286
+ refresh: queue.refresh,
287
+ setGlobalProject: setProjects,
288
+ })
289
+ if (event.type === "server.connected" || event.type === "global.disposed") {
290
+ for (const directory of Object.keys(children.children)) {
291
+ queue.push(directory)
292
+ }
293
+ }
294
+ return
295
+ }
296
+
297
+ const existing = children.children[directory]
298
+ if (!existing) return
299
+ children.mark(directory)
300
+ const [store, setStore] = existing
301
+ applyDirectoryEvent({
302
+ event,
303
+ directory,
304
+ store,
305
+ setStore,
306
+ push: queue.push,
307
+ setSessionTodo,
308
+ vcsCache: children.vcsCache.get(directory),
309
+ loadLsp: () => {
310
+ sdkFor(directory)
311
+ .lsp.status()
312
+ .then((x) => setStore("lsp", x.data ?? []))
313
+ },
314
+ })
315
+ })
316
+
317
+ onCleanup(unsub)
318
+ onCleanup(() => {
319
+ queue.dispose()
320
+ })
321
+ onCleanup(() => {
322
+ for (const directory of Object.keys(children.children)) {
323
+ children.disposeDirectory(directory)
324
+ }
325
+ })
326
+
327
+ async function bootstrap() {
328
+ await bootstrapGlobal({
329
+ globalSDK: globalSDK.client,
330
+ connectErrorTitle: language.t("dialog.server.add.error"),
331
+ connectErrorDescription: language.t("error.globalSync.connectFailed", {
332
+ url: globalSDK.url,
333
+ }),
334
+ requestFailedTitle: language.t("common.requestFailed"),
335
+ translate: language.t,
336
+ formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
337
+ setGlobalStore: setBootStore,
338
+ })
339
+ }
340
+
341
+ onMount(() => {
342
+ void bootstrap()
343
+ })
344
+
345
+ const projectApi = {
346
+ loadSessions,
347
+ meta(directory: string, patch: ProjectMeta) {
348
+ children.projectMeta(directory, patch)
349
+ },
350
+ icon(directory: string, value: string | undefined) {
351
+ children.projectIcon(directory, value)
352
+ },
353
+ }
354
+
355
+ const updateConfig = async (config: Config) => {
356
+ setGlobalStore("reload", "pending")
357
+ return globalSDK.client.global.config
358
+ .update({ config })
359
+ .then(bootstrap)
360
+ .then(() => {
361
+ queue.refresh()
362
+ setGlobalStore("reload", undefined)
363
+ queue.refresh()
364
+ })
365
+ .catch((error) => {
366
+ setGlobalStore("reload", undefined)
367
+ throw error
368
+ })
369
+ }
370
+
371
+ return {
372
+ data: globalStore,
373
+ set,
374
+ get ready() {
375
+ return globalStore.ready
376
+ },
377
+ get error() {
378
+ return globalStore.error
379
+ },
380
+ child: children.child,
381
+ peek: children.peek,
382
+ bootstrap,
383
+ updateConfig,
384
+ project: projectApi,
385
+ todo: {
386
+ set: setSessionTodo,
387
+ },
388
+ }
389
+ }
390
+
391
+ const GlobalSyncContext = createContext<ReturnType<typeof createGlobalSync>>()
392
+
393
+ export function GlobalSyncProvider(props: ParentProps) {
394
+ const value = createGlobalSync()
395
+ return (
396
+ <Switch>
397
+ <Match when={value.ready}>
398
+ <GlobalSyncContext.Provider value={value}>{props.children}</GlobalSyncContext.Provider>
399
+ </Match>
400
+ </Switch>
401
+ )
402
+ }
403
+
404
+ export function useGlobalSync() {
405
+ const context = useContext(GlobalSyncContext)
406
+ if (!context) throw new Error("useGlobalSync must be used within GlobalSyncProvider")
407
+ return context
408
+ }
@@ -0,0 +1,233 @@
1
+ import { createEffect, onCleanup } from "solid-js"
2
+ import { createStore } from "solid-js/store"
3
+ import { createSimpleContext } from "@reign-labs/ui/context"
4
+ import { useDialog } from "@reign-labs/ui/context/dialog"
5
+ import { usePlatform } from "@/context/platform"
6
+ import { useSettings } from "@/context/settings"
7
+ import { persisted } from "@/utils/persist"
8
+ import { DialogReleaseNotes, type Highlight } from "@/components/dialog-release-notes"
9
+
10
+ const CHANGELOG_URL = "https://code.reign-labs.com/changelog.json"
11
+
12
+ type Store = {
13
+ version?: string
14
+ }
15
+
16
+ type ParsedRelease = {
17
+ tag?: string
18
+ highlights: Highlight[]
19
+ }
20
+
21
+ function isRecord(value: unknown): value is Record<string, unknown> {
22
+ return typeof value === "object" && value !== null && !Array.isArray(value)
23
+ }
24
+
25
+ function getText(value: unknown): string | undefined {
26
+ if (typeof value === "string") {
27
+ const text = value.trim()
28
+ return text.length > 0 ? text : undefined
29
+ }
30
+
31
+ if (typeof value === "number") return String(value)
32
+ return
33
+ }
34
+
35
+ function normalizeVersion(value: string | undefined) {
36
+ const text = value?.trim()
37
+ if (!text) return
38
+ return text.startsWith("v") || text.startsWith("V") ? text.slice(1) : text
39
+ }
40
+
41
+ function parseMedia(value: unknown, alt: string): Highlight["media"] | undefined {
42
+ if (!isRecord(value)) return
43
+ const type = getText(value.type)?.toLowerCase()
44
+ const src = getText(value.src) ?? getText(value.url)
45
+ if (!src) return
46
+ if (type !== "image" && type !== "video") return
47
+
48
+ return { type, src, alt }
49
+ }
50
+
51
+ function parseHighlight(value: unknown): Highlight | undefined {
52
+ if (!isRecord(value)) return
53
+
54
+ const title = getText(value.title)
55
+ if (!title) return
56
+
57
+ const description = getText(value.description) ?? getText(value.shortDescription)
58
+ if (!description) return
59
+
60
+ const media = parseMedia(value.media, title)
61
+ return { title, description, media }
62
+ }
63
+
64
+ function parseRelease(value: unknown): ParsedRelease | undefined {
65
+ if (!isRecord(value)) return
66
+ const tag = getText(value.tag) ?? getText(value.tag_name) ?? getText(value.name)
67
+
68
+ if (!Array.isArray(value.highlights)) {
69
+ return { tag, highlights: [] }
70
+ }
71
+
72
+ const highlights = value.highlights.flatMap((group) => {
73
+ if (!isRecord(group)) return []
74
+
75
+ const source = getText(group.source)
76
+ if (!source) return []
77
+ if (!source.toLowerCase().includes("desktop")) return []
78
+
79
+ if (Array.isArray(group.items)) {
80
+ return group.items.map((item) => parseHighlight(item)).filter((item): item is Highlight => item !== undefined)
81
+ }
82
+
83
+ const item = parseHighlight(group)
84
+ if (!item) return []
85
+ return [item]
86
+ })
87
+
88
+ return { tag, highlights }
89
+ }
90
+
91
+ function parseChangelog(value: unknown): ParsedRelease[] | undefined {
92
+ if (Array.isArray(value)) {
93
+ return value.map(parseRelease).filter((release): release is ParsedRelease => release !== undefined)
94
+ }
95
+
96
+ if (!isRecord(value)) return
97
+ if (!Array.isArray(value.releases)) return
98
+
99
+ return value.releases.map(parseRelease).filter((release): release is ParsedRelease => release !== undefined)
100
+ }
101
+
102
+ function sliceHighlights(input: { releases: ParsedRelease[]; current?: string; previous?: string }) {
103
+ const current = normalizeVersion(input.current)
104
+ const previous = normalizeVersion(input.previous)
105
+ const releases = input.releases
106
+
107
+ const start = (() => {
108
+ if (!current) return 0
109
+ const index = releases.findIndex((release) => normalizeVersion(release.tag) === current)
110
+ return index === -1 ? 0 : index
111
+ })()
112
+
113
+ const end = (() => {
114
+ if (!previous) return releases.length
115
+ const index = releases.findIndex((release, i) => i >= start && normalizeVersion(release.tag) === previous)
116
+ return index === -1 ? releases.length : index
117
+ })()
118
+
119
+ const highlights = releases.slice(start, end).flatMap((release) => release.highlights)
120
+ const seen = new Set<string>()
121
+ const unique = highlights.filter((highlight) => {
122
+ const key = dedupeKey(highlight)
123
+ if (seen.has(key)) return false
124
+ seen.add(key)
125
+ return true
126
+ })
127
+ return unique.slice(0, 5)
128
+ }
129
+
130
+ function dedupeKey(highlight: Highlight) {
131
+ return [highlight.title, highlight.description, highlight.media?.type ?? "", highlight.media?.src ?? ""].join("\n")
132
+ }
133
+
134
+ function loadReleaseHighlights(value: unknown, current?: string, previous?: string) {
135
+ const releases = parseChangelog(value)
136
+ if (!releases?.length) return []
137
+ return sliceHighlights({ releases, current, previous })
138
+ }
139
+
140
+ export const { use: useHighlights, provider: HighlightsProvider } = createSimpleContext({
141
+ name: "Highlights",
142
+ gate: false,
143
+ init: () => {
144
+ const platform = usePlatform()
145
+ const dialog = useDialog()
146
+ const settings = useSettings()
147
+ const [store, setStore, _, ready] = persisted("highlights.v1", createStore<Store>({ version: undefined }))
148
+
149
+ const [range, setRange] = createStore({
150
+ from: undefined as string | undefined,
151
+ to: undefined as string | undefined,
152
+ })
153
+ const state = { started: false }
154
+ let timer: ReturnType<typeof setTimeout> | undefined
155
+
156
+ const clearTimer = () => {
157
+ if (timer === undefined) return
158
+ clearTimeout(timer)
159
+ timer = undefined
160
+ }
161
+
162
+ const markSeen = () => {
163
+ if (!platform.version) return
164
+ setStore("version", platform.version)
165
+ }
166
+
167
+ const start = (previous: string) => {
168
+ if (!settings.general.releaseNotes()) {
169
+ markSeen()
170
+ return
171
+ }
172
+
173
+ const fetcher = platform.fetch ?? fetch
174
+ const controller = new AbortController()
175
+ onCleanup(() => {
176
+ controller.abort()
177
+ clearTimer()
178
+ })
179
+
180
+ fetcher(CHANGELOG_URL, {
181
+ signal: controller.signal,
182
+ headers: { Accept: "application/json" },
183
+ })
184
+ .then((response) => (response.ok ? (response.json() as Promise<unknown>) : undefined))
185
+ .then((json) => {
186
+ if (!json) return
187
+ const highlights = loadReleaseHighlights(json, platform.version, previous)
188
+ if (controller.signal.aborted) return
189
+
190
+ if (highlights.length === 0) {
191
+ markSeen()
192
+ return
193
+ }
194
+
195
+ timer = setTimeout(() => {
196
+ timer = undefined
197
+ markSeen()
198
+ dialog.show(() => <DialogReleaseNotes highlights={highlights} />)
199
+ }, 500)
200
+ })
201
+ .catch(() => undefined)
202
+ }
203
+
204
+ createEffect(() => {
205
+ if (state.started) return
206
+ if (!ready()) return
207
+ if (!settings.ready()) return
208
+ if (!platform.version) return
209
+ state.started = true
210
+
211
+ const previous = store.version
212
+ if (!previous) {
213
+ setStore("version", platform.version)
214
+ return
215
+ }
216
+
217
+ if (previous === platform.version) return
218
+
219
+ setRange({ from: previous, to: platform.version })
220
+ start(previous)
221
+ })
222
+
223
+ return {
224
+ ready,
225
+ from: () => range.from,
226
+ to: () => range.to,
227
+ get last() {
228
+ return store.version
229
+ },
230
+ markSeen,
231
+ }
232
+ },
233
+ })