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,277 @@
1
+ import { createEffect, createMemo, onCleanup } from "solid-js"
2
+ import { createStore, produce } from "solid-js/store"
3
+ import { createSimpleContext } from "@reign-labs/ui/context"
4
+ import type { PermissionRequest } from "@reign-labs/sdk/v2/client"
5
+ import { Persist, persisted } from "@/utils/persist"
6
+ import { useGlobalSDK } from "@/context/global-sdk"
7
+ import { useGlobalSync } from "./global-sync"
8
+ import { useParams } from "@solidjs/router"
9
+ import { decode64 } from "@/utils/base64"
10
+ import {
11
+ acceptKey,
12
+ directoryAcceptKey,
13
+ isDirectoryAutoAccepting,
14
+ autoRespondsPermission,
15
+ } from "./permission-auto-respond"
16
+
17
+ type PermissionRespondFn = (input: {
18
+ sessionID: string
19
+ permissionID: string
20
+ response: "once" | "always" | "reject"
21
+ directory?: string
22
+ }) => void
23
+
24
+ function isNonAllowRule(rule: unknown) {
25
+ if (!rule) return false
26
+ if (typeof rule === "string") return rule !== "allow"
27
+ if (typeof rule !== "object") return false
28
+ if (Array.isArray(rule)) return false
29
+
30
+ for (const action of Object.values(rule)) {
31
+ if (action !== "allow") return true
32
+ }
33
+
34
+ return false
35
+ }
36
+
37
+ function hasPermissionPromptRules(permission: unknown) {
38
+ if (!permission) return false
39
+ if (typeof permission === "string") return permission !== "allow"
40
+ if (typeof permission !== "object") return false
41
+ if (Array.isArray(permission)) return false
42
+
43
+ const config = permission as Record<string, unknown>
44
+ return Object.values(config).some(isNonAllowRule)
45
+ }
46
+
47
+ export const { use: usePermission, provider: PermissionProvider } = createSimpleContext({
48
+ name: "Permission",
49
+ init: () => {
50
+ const params = useParams()
51
+ const globalSDK = useGlobalSDK()
52
+ const globalSync = useGlobalSync()
53
+
54
+ const permissionsEnabled = createMemo(() => {
55
+ const directory = decode64(params.dir)
56
+ if (!directory) return false
57
+ const [store] = globalSync.child(directory)
58
+ return hasPermissionPromptRules(store.config.permission)
59
+ })
60
+
61
+ const [store, setStore, _, ready] = persisted(
62
+ {
63
+ ...Persist.global("permission", ["permission.v3"]),
64
+ migrate(value) {
65
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value
66
+
67
+ const data = value as Record<string, unknown>
68
+ if (data.autoAccept) return value
69
+
70
+ return {
71
+ ...data,
72
+ autoAccept:
73
+ typeof data.autoAcceptEdits === "object" && data.autoAcceptEdits && !Array.isArray(data.autoAcceptEdits)
74
+ ? data.autoAcceptEdits
75
+ : {},
76
+ }
77
+ },
78
+ },
79
+ createStore({
80
+ autoAccept: {} as Record<string, boolean>,
81
+ }),
82
+ )
83
+
84
+ // When config has permission: "allow", auto-enable directory-level auto-accept
85
+ createEffect(() => {
86
+ if (!ready()) return
87
+ const directory = decode64(params.dir)
88
+ if (!directory) return
89
+ const [childStore] = globalSync.child(directory)
90
+ const perm = childStore.config.permission
91
+ if (typeof perm === "string" && perm === "allow") {
92
+ const key = directoryAcceptKey(directory)
93
+ if (store.autoAccept[key] === undefined) {
94
+ setStore(
95
+ produce((draft) => {
96
+ draft.autoAccept[key] = true
97
+ }),
98
+ )
99
+ }
100
+ }
101
+ })
102
+
103
+ const MAX_RESPONDED = 1000
104
+ const RESPONDED_TTL_MS = 60 * 60 * 1000
105
+ const responded = new Map<string, number>()
106
+ const enableVersion = new Map<string, number>()
107
+
108
+ function pruneResponded(now: number) {
109
+ for (const [id, ts] of responded) {
110
+ if (now - ts < RESPONDED_TTL_MS) break
111
+ responded.delete(id)
112
+ }
113
+
114
+ for (const id of responded.keys()) {
115
+ if (responded.size <= MAX_RESPONDED) break
116
+ responded.delete(id)
117
+ }
118
+ }
119
+
120
+ const respond: PermissionRespondFn = (input) => {
121
+ globalSDK.client.permission.respond(input).catch(() => {
122
+ responded.delete(input.permissionID)
123
+ })
124
+ }
125
+
126
+ function respondOnce(permission: PermissionRequest, directory?: string) {
127
+ const now = Date.now()
128
+ const hit = responded.has(permission.id)
129
+ responded.delete(permission.id)
130
+ responded.set(permission.id, now)
131
+ pruneResponded(now)
132
+ if (hit) return
133
+ respond({
134
+ sessionID: permission.sessionID,
135
+ permissionID: permission.id,
136
+ response: "once",
137
+ directory,
138
+ })
139
+ }
140
+
141
+ function isAutoAccepting(sessionID: string, directory?: string) {
142
+ const session = directory ? globalSync.child(directory, { bootstrap: false })[0].session : []
143
+ return autoRespondsPermission(store.autoAccept, session, { sessionID }, directory)
144
+ }
145
+
146
+ function isAutoAcceptingDirectory(directory: string) {
147
+ return isDirectoryAutoAccepting(store.autoAccept, directory)
148
+ }
149
+
150
+ function shouldAutoRespond(permission: PermissionRequest, directory?: string) {
151
+ const session = directory ? globalSync.child(directory, { bootstrap: false })[0].session : []
152
+ return autoRespondsPermission(store.autoAccept, session, permission, directory)
153
+ }
154
+
155
+ function bumpEnableVersion(sessionID: string, directory?: string) {
156
+ const key = acceptKey(sessionID, directory)
157
+ const next = (enableVersion.get(key) ?? 0) + 1
158
+ enableVersion.set(key, next)
159
+ return next
160
+ }
161
+
162
+ const unsubscribe = globalSDK.event.listen((e) => {
163
+ const event = e.details
164
+ if (event?.type !== "permission.asked") return
165
+
166
+ const perm = event.properties
167
+ if (!shouldAutoRespond(perm, e.name)) return
168
+
169
+ respondOnce(perm, e.name)
170
+ })
171
+ onCleanup(unsubscribe)
172
+
173
+ function enableDirectory(directory: string) {
174
+ const key = directoryAcceptKey(directory)
175
+ setStore(
176
+ produce((draft) => {
177
+ draft.autoAccept[key] = true
178
+ }),
179
+ )
180
+
181
+ globalSDK.client.permission
182
+ .list({ directory })
183
+ .then((x) => {
184
+ if (!isAutoAcceptingDirectory(directory)) return
185
+ for (const perm of x.data ?? []) {
186
+ if (!perm?.id) continue
187
+ if (!shouldAutoRespond(perm, directory)) continue
188
+ respondOnce(perm, directory)
189
+ }
190
+ })
191
+ .catch(() => undefined)
192
+ }
193
+
194
+ function disableDirectory(directory: string) {
195
+ const key = directoryAcceptKey(directory)
196
+ setStore(
197
+ produce((draft) => {
198
+ draft.autoAccept[key] = false
199
+ }),
200
+ )
201
+ }
202
+
203
+ function enable(sessionID: string, directory: string) {
204
+ const key = acceptKey(sessionID, directory)
205
+ const version = bumpEnableVersion(sessionID, directory)
206
+ setStore(
207
+ produce((draft) => {
208
+ draft.autoAccept[key] = true
209
+ delete draft.autoAccept[sessionID]
210
+ }),
211
+ )
212
+
213
+ globalSDK.client.permission
214
+ .list({ directory })
215
+ .then((x) => {
216
+ if (enableVersion.get(key) !== version) return
217
+ if (!isAutoAccepting(sessionID, directory)) return
218
+ for (const perm of x.data ?? []) {
219
+ if (!perm?.id) continue
220
+ if (!shouldAutoRespond(perm, directory)) continue
221
+ respondOnce(perm, directory)
222
+ }
223
+ })
224
+ .catch(() => undefined)
225
+ }
226
+
227
+ function disable(sessionID: string, directory?: string) {
228
+ bumpEnableVersion(sessionID, directory)
229
+ const key = directory ? acceptKey(sessionID, directory) : sessionID
230
+ setStore(
231
+ produce((draft) => {
232
+ draft.autoAccept[key] = false
233
+ if (!directory) return
234
+ delete draft.autoAccept[sessionID]
235
+ }),
236
+ )
237
+ }
238
+
239
+ return {
240
+ ready,
241
+ respond,
242
+ autoResponds(permission: PermissionRequest, directory?: string) {
243
+ return shouldAutoRespond(permission, directory)
244
+ },
245
+ isAutoAccepting,
246
+ isAutoAcceptingDirectory,
247
+ toggleAutoAccept(sessionID: string, directory: string) {
248
+ if (isAutoAccepting(sessionID, directory)) {
249
+ disable(sessionID, directory)
250
+ return
251
+ }
252
+
253
+ enable(sessionID, directory)
254
+ },
255
+ toggleAutoAcceptDirectory(directory: string) {
256
+ if (isAutoAcceptingDirectory(directory)) {
257
+ disableDirectory(directory)
258
+ return
259
+ }
260
+ enableDirectory(directory)
261
+ },
262
+ enableAutoAccept(sessionID: string, directory: string) {
263
+ if (isAutoAccepting(sessionID, directory)) return
264
+ enable(sessionID, directory)
265
+ },
266
+ disableAutoAccept(sessionID: string, directory?: string) {
267
+ disable(sessionID, directory)
268
+ },
269
+ permissionsEnabled,
270
+ isPermissionAllowAll(directory: string) {
271
+ const [childStore] = globalSync.child(directory)
272
+ const perm = childStore.config.permission
273
+ return typeof perm === "string" && perm === "allow"
274
+ },
275
+ }
276
+ },
277
+ })
@@ -0,0 +1,99 @@
1
+ import { createSimpleContext } from "@reign-labs/ui/context"
2
+ import type { AsyncStorage, SyncStorage } from "@solid-primitives/storage"
3
+ import type { Accessor } from "solid-js"
4
+ import { ServerConnection } from "./server"
5
+
6
+ type PickerPaths = string | string[] | null
7
+ type OpenDirectoryPickerOptions = { title?: string; multiple?: boolean }
8
+ type OpenFilePickerOptions = { title?: string; multiple?: boolean; accept?: string[]; extensions?: string[] }
9
+ type SaveFilePickerOptions = { title?: string; defaultPath?: string }
10
+ type UpdateInfo = { updateAvailable: boolean; version?: string }
11
+
12
+ export type Platform = {
13
+ /** Platform discriminator */
14
+ platform: "web" | "desktop"
15
+
16
+ /** Desktop OS (Tauri only) */
17
+ os?: "macos" | "windows" | "linux"
18
+
19
+ /** App version */
20
+ version?: string
21
+
22
+ /** Open a URL in the default browser */
23
+ openLink(url: string): void
24
+
25
+ /** Open a local path in a local app (desktop only) */
26
+ openPath?(path: string, app?: string): Promise<void>
27
+
28
+ /** Restart the app */
29
+ restart(): Promise<void>
30
+
31
+ /** Navigate back in history */
32
+ back(): void
33
+
34
+ /** Navigate forward in history */
35
+ forward(): void
36
+
37
+ /** Send a system notification (optional deep link) */
38
+ notify(title: string, description?: string, href?: string): Promise<void>
39
+
40
+ /** Open directory picker dialog (native on Tauri, server-backed on web) */
41
+ openDirectoryPickerDialog?(opts?: OpenDirectoryPickerOptions): Promise<PickerPaths>
42
+
43
+ /** Open native file picker dialog (Tauri only) */
44
+ openFilePickerDialog?(opts?: OpenFilePickerOptions): Promise<PickerPaths>
45
+
46
+ /** Save file picker dialog (Tauri only) */
47
+ saveFilePickerDialog?(opts?: SaveFilePickerOptions): Promise<string | null>
48
+
49
+ /** Storage mechanism, defaults to localStorage */
50
+ storage?: (name?: string) => SyncStorage | AsyncStorage
51
+
52
+ /** Check for updates (Tauri only) */
53
+ checkUpdate?(): Promise<UpdateInfo>
54
+
55
+ /** Install updates (Tauri only) */
56
+ update?(): Promise<void>
57
+
58
+ /** Fetch override */
59
+ fetch?: typeof fetch
60
+
61
+ /** Get the configured default server URL (platform-specific) */
62
+ getDefaultServer?(): Promise<ServerConnection.Key | null>
63
+
64
+ /** Set the default server URL to use on app startup (platform-specific) */
65
+ setDefaultServer?(url: ServerConnection.Key | null): Promise<void> | void
66
+
67
+ /** Get the configured WSL integration (desktop only) */
68
+ getWslEnabled?(): Promise<boolean>
69
+
70
+ /** Set the configured WSL integration (desktop only) */
71
+ setWslEnabled?(config: boolean): Promise<void> | void
72
+
73
+ /** Get the preferred display backend (desktop only) */
74
+ getDisplayBackend?(): Promise<DisplayBackend | null> | DisplayBackend | null
75
+
76
+ /** Set the preferred display backend (desktop only) */
77
+ setDisplayBackend?(backend: DisplayBackend): Promise<void>
78
+
79
+ /** Parse markdown to HTML using native parser (desktop only, returns unprocessed code blocks) */
80
+ parseMarkdown?(markdown: string): Promise<string>
81
+
82
+ /** Webview zoom level (desktop only) */
83
+ webviewZoom?: Accessor<number>
84
+
85
+ /** Check if an editor app exists (desktop only) */
86
+ checkAppExists?(appName: string): Promise<boolean>
87
+
88
+ /** Read image from clipboard (desktop only) */
89
+ readClipboardImage?(): Promise<File | null>
90
+ }
91
+
92
+ export type DisplayBackend = "auto" | "wayland"
93
+
94
+ export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({
95
+ name: "Platform",
96
+ init: (props: { value: Platform }) => {
97
+ return props.value
98
+ },
99
+ })
@@ -0,0 +1,297 @@
1
+ import { createSimpleContext } from "@reign-labs/ui/context"
2
+ import { checksum } from "@reign-labs/util/encode"
3
+ import { useParams } from "@solidjs/router"
4
+ import { batch, createMemo, createRoot, getOwner, onCleanup } from "solid-js"
5
+ import { createStore, type SetStoreFunction } from "solid-js/store"
6
+ import type { FileSelection } from "@/context/file"
7
+ import { Persist, persisted } from "@/utils/persist"
8
+
9
+ interface PartBase {
10
+ content: string
11
+ start: number
12
+ end: number
13
+ }
14
+
15
+ export interface TextPart extends PartBase {
16
+ type: "text"
17
+ }
18
+
19
+ export interface FileAttachmentPart extends PartBase {
20
+ type: "file"
21
+ path: string
22
+ selection?: FileSelection
23
+ }
24
+
25
+ export interface AgentPart extends PartBase {
26
+ type: "agent"
27
+ name: string
28
+ }
29
+
30
+ export interface ImageAttachmentPart {
31
+ type: "image"
32
+ id: string
33
+ filename: string
34
+ mime: string
35
+ dataUrl: string
36
+ }
37
+
38
+ export type ContentPart = TextPart | FileAttachmentPart | AgentPart | ImageAttachmentPart
39
+ export type Prompt = ContentPart[]
40
+
41
+ export type FileContextItem = {
42
+ type: "file"
43
+ path: string
44
+ selection?: FileSelection
45
+ comment?: string
46
+ commentID?: string
47
+ commentOrigin?: "review" | "file"
48
+ preview?: string
49
+ }
50
+
51
+ export type ContextItem = FileContextItem
52
+
53
+ export const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
54
+
55
+ function isSelectionEqual(a?: FileSelection, b?: FileSelection) {
56
+ if (!a && !b) return true
57
+ if (!a || !b) return false
58
+ return (
59
+ a.startLine === b.startLine && a.startChar === b.startChar && a.endLine === b.endLine && a.endChar === b.endChar
60
+ )
61
+ }
62
+
63
+ function isPartEqual(partA: ContentPart, partB: ContentPart) {
64
+ switch (partA.type) {
65
+ case "text":
66
+ return partB.type === "text" && partA.content === partB.content
67
+ case "file":
68
+ return partB.type === "file" && partA.path === partB.path && isSelectionEqual(partA.selection, partB.selection)
69
+ case "agent":
70
+ return partB.type === "agent" && partA.name === partB.name
71
+ case "image":
72
+ return partB.type === "image" && partA.id === partB.id
73
+ }
74
+ }
75
+
76
+ export function isPromptEqual(promptA: Prompt, promptB: Prompt): boolean {
77
+ if (promptA.length !== promptB.length) return false
78
+ for (let i = 0; i < promptA.length; i++) {
79
+ if (!isPartEqual(promptA[i], promptB[i])) return false
80
+ }
81
+ return true
82
+ }
83
+
84
+ function cloneSelection(selection?: FileSelection) {
85
+ if (!selection) return undefined
86
+ return { ...selection }
87
+ }
88
+
89
+ function clonePart(part: ContentPart): ContentPart {
90
+ if (part.type === "text") return { ...part }
91
+ if (part.type === "image") return { ...part }
92
+ if (part.type === "agent") return { ...part }
93
+ return {
94
+ ...part,
95
+ selection: cloneSelection(part.selection),
96
+ }
97
+ }
98
+
99
+ function clonePrompt(prompt: Prompt): Prompt {
100
+ return prompt.map(clonePart)
101
+ }
102
+
103
+ function contextItemKey(item: ContextItem) {
104
+ if (item.type !== "file") return item.type
105
+ const start = item.selection?.startLine
106
+ const end = item.selection?.endLine
107
+ const key = `${item.type}:${item.path}:${start}:${end}`
108
+
109
+ if (item.commentID) {
110
+ return `${key}:c=${item.commentID}`
111
+ }
112
+
113
+ const comment = item.comment?.trim()
114
+ if (!comment) return key
115
+ const digest = checksum(comment) ?? comment
116
+ return `${key}:c=${digest.slice(0, 8)}`
117
+ }
118
+
119
+ function isCommentItem(item: ContextItem | (ContextItem & { key: string })) {
120
+ return item.type === "file" && !!item.comment?.trim()
121
+ }
122
+
123
+ function createPromptActions(
124
+ setStore: SetStoreFunction<{
125
+ prompt: Prompt
126
+ cursor?: number
127
+ context: {
128
+ items: (ContextItem & { key: string })[]
129
+ }
130
+ }>,
131
+ ) {
132
+ return {
133
+ set(prompt: Prompt, cursorPosition?: number) {
134
+ const next = clonePrompt(prompt)
135
+ batch(() => {
136
+ setStore("prompt", next)
137
+ if (cursorPosition !== undefined) setStore("cursor", cursorPosition)
138
+ })
139
+ },
140
+ reset() {
141
+ batch(() => {
142
+ setStore("prompt", clonePrompt(DEFAULT_PROMPT))
143
+ setStore("cursor", 0)
144
+ })
145
+ },
146
+ }
147
+ }
148
+
149
+ const WORKSPACE_KEY = "__workspace__"
150
+ const MAX_PROMPT_SESSIONS = 20
151
+
152
+ type PromptSession = ReturnType<typeof createPromptSession>
153
+
154
+ type Scope = {
155
+ dir: string
156
+ id?: string
157
+ }
158
+
159
+ type PromptCacheEntry = {
160
+ value: PromptSession
161
+ dispose: VoidFunction
162
+ }
163
+
164
+ function createPromptSession(dir: string, id: string | undefined) {
165
+ const legacy = `${dir}/prompt${id ? "/" + id : ""}.v2`
166
+
167
+ const [store, setStore, _, ready] = persisted(
168
+ Persist.scoped(dir, id, "prompt", [legacy]),
169
+ createStore<{
170
+ prompt: Prompt
171
+ cursor?: number
172
+ context: {
173
+ items: (ContextItem & { key: string })[]
174
+ }
175
+ }>({
176
+ prompt: clonePrompt(DEFAULT_PROMPT),
177
+ cursor: undefined,
178
+ context: {
179
+ items: [],
180
+ },
181
+ }),
182
+ )
183
+
184
+ const actions = createPromptActions(setStore)
185
+
186
+ return {
187
+ ready,
188
+ current: createMemo(() => store.prompt),
189
+ cursor: createMemo(() => store.cursor),
190
+ dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
191
+ context: {
192
+ items: createMemo(() => store.context.items),
193
+ add(item: ContextItem) {
194
+ const key = contextItemKey(item)
195
+ if (store.context.items.find((x) => x.key === key)) return
196
+ setStore("context", "items", (items) => [...items, { key, ...item }])
197
+ },
198
+ remove(key: string) {
199
+ setStore("context", "items", (items) => items.filter((x) => x.key !== key))
200
+ },
201
+ removeComment(path: string, commentID: string) {
202
+ setStore("context", "items", (items) =>
203
+ items.filter((item) => !(item.type === "file" && item.path === path && item.commentID === commentID)),
204
+ )
205
+ },
206
+ updateComment(path: string, commentID: string, next: Partial<FileContextItem> & { comment?: string }) {
207
+ setStore("context", "items", (items) =>
208
+ items.map((item) => {
209
+ if (item.type !== "file" || item.path !== path || item.commentID !== commentID) return item
210
+ const value = { ...item, ...next }
211
+ return { ...value, key: contextItemKey(value) }
212
+ }),
213
+ )
214
+ },
215
+ replaceComments(items: FileContextItem[]) {
216
+ setStore("context", "items", (current) => [
217
+ ...current.filter((item) => !isCommentItem(item)),
218
+ ...items.map((item) => ({ ...item, key: contextItemKey(item) })),
219
+ ])
220
+ },
221
+ },
222
+ set: actions.set,
223
+ reset: actions.reset,
224
+ }
225
+ }
226
+
227
+ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext({
228
+ name: "Prompt",
229
+ gate: false,
230
+ init: () => {
231
+ const params = useParams()
232
+ const cache = new Map<string, PromptCacheEntry>()
233
+
234
+ const disposeAll = () => {
235
+ for (const entry of cache.values()) {
236
+ entry.dispose()
237
+ }
238
+ cache.clear()
239
+ }
240
+
241
+ onCleanup(disposeAll)
242
+
243
+ const prune = () => {
244
+ while (cache.size > MAX_PROMPT_SESSIONS) {
245
+ const first = cache.keys().next().value
246
+ if (!first) return
247
+ const entry = cache.get(first)
248
+ entry?.dispose()
249
+ cache.delete(first)
250
+ }
251
+ }
252
+
253
+ const owner = getOwner()
254
+ const load = (dir: string, id: string | undefined) => {
255
+ const key = `${dir}:${id ?? WORKSPACE_KEY}`
256
+ const existing = cache.get(key)
257
+ if (existing) {
258
+ cache.delete(key)
259
+ cache.set(key, existing)
260
+ return existing.value
261
+ }
262
+
263
+ const entry = createRoot(
264
+ (dispose) => ({
265
+ value: createPromptSession(dir, id),
266
+ dispose,
267
+ }),
268
+ owner,
269
+ )
270
+
271
+ cache.set(key, entry)
272
+ prune()
273
+ return entry.value
274
+ }
275
+
276
+ const session = createMemo(() => load(params.dir!, params.id))
277
+ const pick = (scope?: Scope) => (scope ? load(scope.dir, scope.id) : session())
278
+
279
+ return {
280
+ ready: () => session().ready(),
281
+ current: () => session().current(),
282
+ cursor: () => session().cursor(),
283
+ dirty: () => session().dirty(),
284
+ context: {
285
+ items: () => session().context.items(),
286
+ add: (item: ContextItem) => session().context.add(item),
287
+ remove: (key: string) => session().context.remove(key),
288
+ removeComment: (path: string, commentID: string) => session().context.removeComment(path, commentID),
289
+ updateComment: (path: string, commentID: string, next: Partial<FileContextItem> & { comment?: string }) =>
290
+ session().context.updateComment(path, commentID, next),
291
+ replaceComments: (items: FileContextItem[]) => session().context.replaceComments(items),
292
+ },
293
+ set: (prompt: Prompt, cursorPosition?: number, scope?: Scope) => pick(scope).set(prompt, cursorPosition),
294
+ reset: (scope?: Scope) => pick(scope).reset(),
295
+ }
296
+ },
297
+ })