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,73 @@
1
+ import { Component } from "solid-js"
2
+ import { Dialog } from "@reign-labs/ui/dialog"
3
+ import { Tabs } from "@reign-labs/ui/tabs"
4
+ import { Icon } from "@reign-labs/ui/icon"
5
+ import { useLanguage } from "@/context/language"
6
+ import { usePlatform } from "@/context/platform"
7
+ import { SettingsGeneral } from "./settings-general"
8
+ import { SettingsKeybinds } from "./settings-keybinds"
9
+ import { SettingsProviders } from "./settings-providers"
10
+ import { SettingsModels } from "./settings-models"
11
+
12
+ export const DialogSettings: Component = () => {
13
+ const language = useLanguage()
14
+ const platform = usePlatform()
15
+
16
+ return (
17
+ <Dialog size="x-large" transition>
18
+ <Tabs orientation="vertical" variant="settings" defaultValue="general" class="h-full settings-dialog">
19
+ <Tabs.List>
20
+ <div class="flex flex-col justify-between h-full w-full">
21
+ <div class="flex flex-col gap-3 w-full pt-3">
22
+ <div class="flex flex-col gap-3">
23
+ <div class="flex flex-col gap-1.5">
24
+ <Tabs.SectionTitle>{language.t("settings.section.desktop")}</Tabs.SectionTitle>
25
+ <div class="flex flex-col gap-1.5 w-full">
26
+ <Tabs.Trigger value="general">
27
+ <Icon name="sliders" />
28
+ {language.t("settings.tab.general")}
29
+ </Tabs.Trigger>
30
+ <Tabs.Trigger value="shortcuts">
31
+ <Icon name="keyboard" />
32
+ {language.t("settings.tab.shortcuts")}
33
+ </Tabs.Trigger>
34
+ </div>
35
+ </div>
36
+
37
+ <div class="flex flex-col gap-1.5">
38
+ <Tabs.SectionTitle>{language.t("settings.section.server")}</Tabs.SectionTitle>
39
+ <div class="flex flex-col gap-1.5 w-full">
40
+ <Tabs.Trigger value="providers">
41
+ <Icon name="providers" />
42
+ {language.t("settings.providers.title")}
43
+ </Tabs.Trigger>
44
+ <Tabs.Trigger value="models">
45
+ <Icon name="models" />
46
+ {language.t("settings.models.title")}
47
+ </Tabs.Trigger>
48
+ </div>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ <div class="flex flex-col gap-1 pl-1 py-1 text-12-medium text-text-weak">
53
+ <span>{language.t("app.name.desktop")}</span>
54
+ <span class="text-11-regular">v{platform.version}</span>
55
+ </div>
56
+ </div>
57
+ </Tabs.List>
58
+ <Tabs.Content value="general" class="no-scrollbar">
59
+ <SettingsGeneral />
60
+ </Tabs.Content>
61
+ <Tabs.Content value="shortcuts" class="no-scrollbar">
62
+ <SettingsKeybinds />
63
+ </Tabs.Content>
64
+ <Tabs.Content value="providers" class="no-scrollbar">
65
+ <SettingsProviders />
66
+ </Tabs.Content>
67
+ <Tabs.Content value="models" class="no-scrollbar">
68
+ <SettingsModels />
69
+ </Tabs.Content>
70
+ </Tabs>
71
+ </Dialog>
72
+ )
73
+ }
@@ -0,0 +1,78 @@
1
+ import { beforeAll, describe, expect, mock, test } from "bun:test"
2
+
3
+ let shouldListRoot: typeof import("./file-tree").shouldListRoot
4
+ let shouldListExpanded: typeof import("./file-tree").shouldListExpanded
5
+ let dirsToExpand: typeof import("./file-tree").dirsToExpand
6
+
7
+ beforeAll(async () => {
8
+ mock.module("@solidjs/router", () => ({
9
+ useNavigate: () => () => undefined,
10
+ useParams: () => ({}),
11
+ }))
12
+ mock.module("@/context/file", () => ({
13
+ useFile: () => ({
14
+ tree: {
15
+ state: () => undefined,
16
+ list: () => Promise.resolve(),
17
+ children: () => [],
18
+ expand: () => {},
19
+ collapse: () => {},
20
+ },
21
+ }),
22
+ }))
23
+ mock.module("@reign-labs/ui/collapsible", () => ({
24
+ Collapsible: {
25
+ Trigger: (props: { children?: unknown }) => props.children,
26
+ Content: (props: { children?: unknown }) => props.children,
27
+ },
28
+ }))
29
+ mock.module("@reign-labs/ui/file-icon", () => ({ FileIcon: () => null }))
30
+ mock.module("@reign-labs/ui/icon", () => ({ Icon: () => null }))
31
+ mock.module("@reign-labs/ui/tooltip", () => ({ Tooltip: (props: { children?: unknown }) => props.children }))
32
+ const mod = await import("./file-tree")
33
+ shouldListRoot = mod.shouldListRoot
34
+ shouldListExpanded = mod.shouldListExpanded
35
+ dirsToExpand = mod.dirsToExpand
36
+ })
37
+
38
+ describe("file tree fetch discipline", () => {
39
+ test("root lists on mount unless already loaded or loading", () => {
40
+ expect(shouldListRoot({ level: 0 })).toBe(true)
41
+ expect(shouldListRoot({ level: 0, dir: { loaded: true } })).toBe(false)
42
+ expect(shouldListRoot({ level: 0, dir: { loading: true } })).toBe(false)
43
+ expect(shouldListRoot({ level: 1 })).toBe(false)
44
+ })
45
+
46
+ test("nested dirs list only when expanded and stale", () => {
47
+ expect(shouldListExpanded({ level: 1 })).toBe(false)
48
+ expect(shouldListExpanded({ level: 1, dir: { expanded: false } })).toBe(false)
49
+ expect(shouldListExpanded({ level: 1, dir: { expanded: true } })).toBe(true)
50
+ expect(shouldListExpanded({ level: 1, dir: { expanded: true, loaded: true } })).toBe(false)
51
+ expect(shouldListExpanded({ level: 1, dir: { expanded: true, loading: true } })).toBe(false)
52
+ expect(shouldListExpanded({ level: 0, dir: { expanded: true } })).toBe(false)
53
+ })
54
+
55
+ test("allowed auto-expand picks only collapsed dirs", () => {
56
+ const expanded = new Set<string>()
57
+ const filter = { dirs: new Set(["src", "src/components"]) }
58
+
59
+ const first = dirsToExpand({
60
+ level: 0,
61
+ filter,
62
+ expanded: (dir) => expanded.has(dir),
63
+ })
64
+
65
+ expect(first).toEqual(["src", "src/components"])
66
+
67
+ for (const dir of first) expanded.add(dir)
68
+
69
+ const second = dirsToExpand({
70
+ level: 0,
71
+ filter,
72
+ expanded: (dir) => expanded.has(dir),
73
+ })
74
+
75
+ expect(second).toEqual([])
76
+ expect(dirsToExpand({ level: 1, filter, expanded: () => false })).toEqual([])
77
+ })
78
+ })
@@ -0,0 +1,507 @@
1
+ import { useFile } from "@/context/file"
2
+ import { encodeFilePath } from "@/context/file/path"
3
+ import { Collapsible } from "@reign-labs/ui/collapsible"
4
+ import { FileIcon } from "@reign-labs/ui/file-icon"
5
+ import { Icon } from "@reign-labs/ui/icon"
6
+ import {
7
+ createEffect,
8
+ createMemo,
9
+ For,
10
+ Match,
11
+ on,
12
+ Show,
13
+ splitProps,
14
+ Switch,
15
+ untrack,
16
+ type ComponentProps,
17
+ type JSXElement,
18
+ type ParentProps,
19
+ } from "solid-js"
20
+ import { Dynamic } from "solid-js/web"
21
+ import type { FileNode } from "@reign-labs/sdk/v2"
22
+
23
+ const MAX_DEPTH = 128
24
+
25
+ function pathToFileUrl(filepath: string): string {
26
+ return `file://${encodeFilePath(filepath)}`
27
+ }
28
+
29
+ type Kind = "add" | "del" | "mix"
30
+
31
+ type Filter = {
32
+ files: Set<string>
33
+ dirs: Set<string>
34
+ }
35
+
36
+ export function shouldListRoot(input: { level: number; dir?: { loaded?: boolean; loading?: boolean } }) {
37
+ if (input.level !== 0) return false
38
+ if (input.dir?.loaded) return false
39
+ if (input.dir?.loading) return false
40
+ return true
41
+ }
42
+
43
+ export function shouldListExpanded(input: {
44
+ level: number
45
+ dir?: { expanded?: boolean; loaded?: boolean; loading?: boolean }
46
+ }) {
47
+ if (input.level === 0) return false
48
+ if (!input.dir?.expanded) return false
49
+ if (input.dir.loaded) return false
50
+ if (input.dir.loading) return false
51
+ return true
52
+ }
53
+
54
+ export function dirsToExpand(input: {
55
+ level: number
56
+ filter?: { dirs: Set<string> }
57
+ expanded: (dir: string) => boolean
58
+ }) {
59
+ if (input.level !== 0) return []
60
+ if (!input.filter) return []
61
+ return [...input.filter.dirs].filter((dir) => !input.expanded(dir))
62
+ }
63
+
64
+ const kindLabel = (kind: Kind) => {
65
+ if (kind === "add") return "A"
66
+ if (kind === "del") return "D"
67
+ return "M"
68
+ }
69
+
70
+ const kindTextColor = (kind: Kind) => {
71
+ if (kind === "add") return "color: var(--icon-diff-add-base)"
72
+ if (kind === "del") return "color: var(--icon-diff-delete-base)"
73
+ return "color: var(--icon-diff-modified-base)"
74
+ }
75
+
76
+ const kindDotColor = (kind: Kind) => {
77
+ if (kind === "add") return "background-color: var(--icon-diff-add-base)"
78
+ if (kind === "del") return "background-color: var(--icon-diff-delete-base)"
79
+ return "background-color: var(--icon-diff-modified-base)"
80
+ }
81
+
82
+ const visibleKind = (node: FileNode, kinds?: ReadonlyMap<string, Kind>, marks?: Set<string>) => {
83
+ const kind = kinds?.get(node.path)
84
+ if (!kind) return
85
+ if (!marks?.has(node.path)) return
86
+ return kind
87
+ }
88
+
89
+ const buildDragImage = (target: HTMLElement) => {
90
+ const icon = target.querySelector('[data-component="file-icon"]') ?? target.querySelector("svg")
91
+ const text = target.querySelector("span")
92
+ if (!icon || !text) return
93
+
94
+ const image = document.createElement("div")
95
+ image.className =
96
+ "flex items-center gap-x-2 px-2 py-1 bg-surface-raised-base rounded-md border border-border-base text-12-regular text-text-strong"
97
+ image.style.position = "absolute"
98
+ image.style.top = "-1000px"
99
+ image.innerHTML = (icon as SVGElement).outerHTML + (text as HTMLSpanElement).outerHTML
100
+ return image
101
+ }
102
+
103
+ const withFileDragImage = (event: DragEvent) => {
104
+ const image = buildDragImage(event.currentTarget as HTMLElement)
105
+ if (!image) return
106
+ document.body.appendChild(image)
107
+ event.dataTransfer?.setDragImage(image, 0, 12)
108
+ setTimeout(() => document.body.removeChild(image), 0)
109
+ }
110
+
111
+ const FileTreeNode = (
112
+ p: ParentProps &
113
+ ComponentProps<"div"> &
114
+ ComponentProps<"button"> & {
115
+ node: FileNode
116
+ level: number
117
+ active?: string
118
+ nodeClass?: string
119
+ draggable: boolean
120
+ kinds?: ReadonlyMap<string, Kind>
121
+ marks?: Set<string>
122
+ as?: "div" | "button"
123
+ },
124
+ ) => {
125
+ const [local, rest] = splitProps(p, [
126
+ "node",
127
+ "level",
128
+ "active",
129
+ "nodeClass",
130
+ "draggable",
131
+ "kinds",
132
+ "marks",
133
+ "as",
134
+ "children",
135
+ "class",
136
+ "classList",
137
+ ])
138
+ const kind = () => visibleKind(local.node, local.kinds, local.marks)
139
+ const active = () => !!kind() && !local.node.ignored
140
+ const color = () => {
141
+ const value = kind()
142
+ if (!value) return
143
+ return kindTextColor(value)
144
+ }
145
+
146
+ return (
147
+ <Dynamic
148
+ component={local.as ?? "div"}
149
+ classList={{
150
+ "w-full min-w-0 h-6 flex items-center justify-start gap-x-1.5 rounded-md px-1.5 py-0 text-left hover:bg-surface-raised-base-hover active:bg-surface-base-active transition-colors cursor-pointer": true,
151
+ "bg-surface-base-active": local.node.path === local.active,
152
+ ...(local.classList ?? {}),
153
+ [local.class ?? ""]: !!local.class,
154
+ [local.nodeClass ?? ""]: !!local.nodeClass,
155
+ }}
156
+ style={`padding-left: ${Math.max(0, 8 + local.level * 12 - (local.node.type === "file" ? 24 : 4))}px`}
157
+ draggable={local.draggable}
158
+ onDragStart={(event: DragEvent) => {
159
+ if (!local.draggable) return
160
+ event.dataTransfer?.setData("text/plain", `file:${local.node.path}`)
161
+ event.dataTransfer?.setData("text/uri-list", pathToFileUrl(local.node.path))
162
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = "copy"
163
+ withFileDragImage(event)
164
+ }}
165
+ {...rest}
166
+ >
167
+ {local.children}
168
+ <span
169
+ classList={{
170
+ "flex-1 min-w-0 text-12-medium whitespace-nowrap truncate": true,
171
+ "text-text-weaker": local.node.ignored,
172
+ "text-text-weak": !local.node.ignored && !active(),
173
+ }}
174
+ style={active() ? color() : undefined}
175
+ >
176
+ {local.node.name}
177
+ </span>
178
+ {(() => {
179
+ const value = kind()
180
+ if (!value) return null
181
+ if (local.node.type === "file") {
182
+ return (
183
+ <span class="shrink-0 w-4 text-center text-12-medium" style={kindTextColor(value)}>
184
+ {kindLabel(value)}
185
+ </span>
186
+ )
187
+ }
188
+ return <div class="shrink-0 size-1.5 mr-1.5 rounded-full" style={kindDotColor(value)} />
189
+ })()}
190
+ </Dynamic>
191
+ )
192
+ }
193
+
194
+ export default function FileTree(props: {
195
+ path: string
196
+ class?: string
197
+ nodeClass?: string
198
+ active?: string
199
+ level?: number
200
+ allowed?: readonly string[]
201
+ modified?: readonly string[]
202
+ kinds?: ReadonlyMap<string, Kind>
203
+ draggable?: boolean
204
+ onFileClick?: (file: FileNode) => void
205
+
206
+ _filter?: Filter
207
+ _marks?: Set<string>
208
+ _deeps?: Map<string, number>
209
+ _kinds?: ReadonlyMap<string, Kind>
210
+ _chain?: readonly string[]
211
+ }) {
212
+ const file = useFile()
213
+ const level = props.level ?? 0
214
+ const draggable = () => props.draggable ?? true
215
+
216
+ const key = (p: string) =>
217
+ file
218
+ .normalize(p)
219
+ .replace(/[\\/]+$/, "")
220
+ .replaceAll("\\", "/")
221
+ const chain = props._chain ? [...props._chain, key(props.path)] : [key(props.path)]
222
+
223
+ const filter = createMemo(() => {
224
+ if (props._filter) return props._filter
225
+
226
+ const allowed = props.allowed
227
+ if (!allowed) return
228
+
229
+ const files = new Set(allowed)
230
+ const dirs = new Set<string>()
231
+
232
+ for (const item of allowed) {
233
+ const parts = item.split("/")
234
+ const parents = parts.slice(0, -1)
235
+ for (const [idx] of parents.entries()) {
236
+ const dir = parents.slice(0, idx + 1).join("/")
237
+ if (dir) dirs.add(dir)
238
+ }
239
+ }
240
+
241
+ return { files, dirs }
242
+ })
243
+
244
+ const marks = createMemo(() => {
245
+ if (props._marks) return props._marks
246
+
247
+ const out = new Set<string>()
248
+ for (const item of props.modified ?? []) out.add(item)
249
+ for (const item of props.kinds?.keys() ?? []) out.add(item)
250
+ if (out.size === 0) return
251
+ return out
252
+ })
253
+
254
+ const kinds = createMemo(() => {
255
+ if (props._kinds) return props._kinds
256
+ return props.kinds
257
+ })
258
+
259
+ const deeps = createMemo(() => {
260
+ if (props._deeps) return props._deeps
261
+
262
+ const out = new Map<string, number>()
263
+
264
+ const root = props.path
265
+ if (!(file.tree.state(root)?.expanded ?? false)) return out
266
+
267
+ const seen = new Set<string>()
268
+ const stack: { dir: string; lvl: number; i: number; kids: string[]; max: number }[] = []
269
+
270
+ const push = (dir: string, lvl: number) => {
271
+ const id = key(dir)
272
+ if (seen.has(id)) return
273
+ seen.add(id)
274
+
275
+ const kids = file.tree
276
+ .children(dir)
277
+ .filter((node) => node.type === "directory" && (file.tree.state(node.path)?.expanded ?? false))
278
+ .map((node) => node.path)
279
+
280
+ stack.push({ dir, lvl, i: 0, kids, max: lvl })
281
+ }
282
+
283
+ push(root, level - 1)
284
+
285
+ while (stack.length > 0) {
286
+ const top = stack[stack.length - 1]!
287
+
288
+ if (top.i < top.kids.length) {
289
+ const next = top.kids[top.i]!
290
+ top.i++
291
+ push(next, top.lvl + 1)
292
+ continue
293
+ }
294
+
295
+ out.set(top.dir, top.max)
296
+ stack.pop()
297
+
298
+ const parent = stack[stack.length - 1]
299
+ if (!parent) continue
300
+ parent.max = Math.max(parent.max, top.max)
301
+ }
302
+
303
+ return out
304
+ })
305
+
306
+ createEffect(() => {
307
+ const current = filter()
308
+ const dirs = dirsToExpand({
309
+ level,
310
+ filter: current,
311
+ expanded: (dir) => untrack(() => file.tree.state(dir)?.expanded) ?? false,
312
+ })
313
+ for (const dir of dirs) file.tree.expand(dir)
314
+ })
315
+
316
+ createEffect(
317
+ on(
318
+ () => props.path,
319
+ (path) => {
320
+ const dir = untrack(() => file.tree.state(path))
321
+ if (!shouldListRoot({ level, dir })) return
322
+ void file.tree.list(path)
323
+ },
324
+ { defer: false },
325
+ ),
326
+ )
327
+
328
+ const nodes = createMemo(() => {
329
+ const nodes = file.tree.children(props.path)
330
+ const current = filter()
331
+ if (!current) return nodes
332
+
333
+ const parent = (path: string) => {
334
+ const idx = path.lastIndexOf("/")
335
+ if (idx === -1) return ""
336
+ return path.slice(0, idx)
337
+ }
338
+
339
+ const leaf = (path: string) => {
340
+ const idx = path.lastIndexOf("/")
341
+ return idx === -1 ? path : path.slice(idx + 1)
342
+ }
343
+
344
+ const out = nodes.filter((node) => {
345
+ if (node.type === "file") return current.files.has(node.path)
346
+ return current.dirs.has(node.path)
347
+ })
348
+
349
+ const seen = new Set(out.map((node) => node.path))
350
+
351
+ for (const dir of current.dirs) {
352
+ if (parent(dir) !== props.path) continue
353
+ if (seen.has(dir)) continue
354
+ out.push({
355
+ name: leaf(dir),
356
+ path: dir,
357
+ absolute: dir,
358
+ type: "directory",
359
+ ignored: false,
360
+ })
361
+ seen.add(dir)
362
+ }
363
+
364
+ for (const item of current.files) {
365
+ if (parent(item) !== props.path) continue
366
+ if (seen.has(item)) continue
367
+ out.push({
368
+ name: leaf(item),
369
+ path: item,
370
+ absolute: item,
371
+ type: "file",
372
+ ignored: false,
373
+ })
374
+ seen.add(item)
375
+ }
376
+
377
+ out.sort((a, b) => {
378
+ if (a.type !== b.type) {
379
+ return a.type === "directory" ? -1 : 1
380
+ }
381
+ return a.name.localeCompare(b.name)
382
+ })
383
+
384
+ return out
385
+ })
386
+
387
+ return (
388
+ <div data-component="filetree" class={`flex flex-col gap-0.5 ${props.class ?? ""}`}>
389
+ <For each={nodes()}>
390
+ {(node) => {
391
+ const expanded = () => file.tree.state(node.path)?.expanded ?? false
392
+ const deep = () => deeps().get(node.path) ?? -1
393
+ const kind = () => visibleKind(node, kinds(), marks())
394
+ const active = () => !!kind() && !node.ignored
395
+
396
+ return (
397
+ <Switch>
398
+ <Match when={node.type === "directory"}>
399
+ <Collapsible
400
+ variant="ghost"
401
+ class="w-full"
402
+ data-scope="filetree"
403
+ forceMount={false}
404
+ open={expanded()}
405
+ onOpenChange={(open) => (open ? file.tree.expand(node.path) : file.tree.collapse(node.path))}
406
+ >
407
+ <Collapsible.Trigger>
408
+ <FileTreeNode
409
+ node={node}
410
+ level={level}
411
+ active={props.active}
412
+ nodeClass={props.nodeClass}
413
+ draggable={draggable()}
414
+ kinds={kinds()}
415
+ marks={marks()}
416
+ >
417
+ <div class="size-4 flex items-center justify-center text-icon-weak">
418
+ <Icon name={expanded() ? "chevron-down" : "chevron-right"} size="small" />
419
+ </div>
420
+ </FileTreeNode>
421
+ </Collapsible.Trigger>
422
+ <Collapsible.Content class="relative pt-0.5">
423
+ <div
424
+ classList={{
425
+ "absolute top-0 bottom-0 w-px pointer-events-none bg-border-weak-base opacity-0 transition-opacity duration-150 ease-out motion-reduce:transition-none": true,
426
+ "group-hover/filetree:opacity-100": expanded() && deep() === level,
427
+ "group-hover/filetree:opacity-50": !(expanded() && deep() === level),
428
+ }}
429
+ style={`left: ${Math.max(0, 8 + level * 12 - 4) + 8}px`}
430
+ />
431
+ <Show
432
+ when={level < MAX_DEPTH && !chain.includes(key(node.path))}
433
+ fallback={<div class="px-2 py-1 text-12-regular text-text-weak">...</div>}
434
+ >
435
+ <FileTree
436
+ path={node.path}
437
+ level={level + 1}
438
+ allowed={props.allowed}
439
+ modified={props.modified}
440
+ kinds={props.kinds}
441
+ active={props.active}
442
+ draggable={props.draggable}
443
+ onFileClick={props.onFileClick}
444
+ _filter={filter()}
445
+ _marks={marks()}
446
+ _deeps={deeps()}
447
+ _kinds={kinds()}
448
+ _chain={chain}
449
+ />
450
+ </Show>
451
+ </Collapsible.Content>
452
+ </Collapsible>
453
+ </Match>
454
+ <Match when={node.type === "file"}>
455
+ <FileTreeNode
456
+ node={node}
457
+ level={level}
458
+ active={props.active}
459
+ nodeClass={props.nodeClass}
460
+ draggable={draggable()}
461
+ kinds={kinds()}
462
+ marks={marks()}
463
+ as="button"
464
+ type="button"
465
+ onClick={() => props.onFileClick?.(node)}
466
+ >
467
+ <div class="w-4 shrink-0" />
468
+ <Switch>
469
+ <Match when={node.ignored}>
470
+ <FileIcon
471
+ node={node}
472
+ class="size-4 filetree-icon filetree-icon--mono"
473
+ style="color: var(--icon-weak-base)"
474
+ mono
475
+ />
476
+ </Match>
477
+ <Match when={active()}>
478
+ <FileIcon
479
+ node={node}
480
+ class="size-4 filetree-icon filetree-icon--mono"
481
+ style={kindTextColor(kind()!)}
482
+ mono
483
+ />
484
+ </Match>
485
+ <Match when={!node.ignored}>
486
+ <span class="filetree-iconpair size-4">
487
+ <FileIcon
488
+ node={node}
489
+ class="size-4 filetree-icon filetree-icon--color opacity-0 group-hover/filetree:opacity-100"
490
+ />
491
+ <FileIcon
492
+ node={node}
493
+ class="size-4 filetree-icon filetree-icon--mono group-hover/filetree:opacity-0"
494
+ mono
495
+ />
496
+ </span>
497
+ </Match>
498
+ </Switch>
499
+ </FileTreeNode>
500
+ </Match>
501
+ </Switch>
502
+ )
503
+ }}
504
+ </For>
505
+ </div>
506
+ )
507
+ }
@@ -0,0 +1,26 @@
1
+ import { ComponentProps, splitProps } from "solid-js"
2
+ import { usePlatform } from "@/context/platform"
3
+
4
+ export interface LinkProps extends Omit<ComponentProps<"a">, "href"> {
5
+ href: string
6
+ }
7
+
8
+ export function Link(props: LinkProps) {
9
+ const platform = usePlatform()
10
+ const [local, rest] = splitProps(props, ["href", "children", "class"])
11
+
12
+ return (
13
+ <a
14
+ href={local.href}
15
+ class={`text-text-strong underline ${local.class ?? ""}`}
16
+ onClick={(event) => {
17
+ if (!local.href) return
18
+ event.preventDefault()
19
+ platform.openLink(local.href)
20
+ }}
21
+ {...rest}
22
+ >
23
+ {local.children}
24
+ </a>
25
+ )
26
+ }