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,453 @@
1
+ import { Component, For, Show, createMemo, onCleanup, onMount } from "solid-js"
2
+ import { createStore } from "solid-js/store"
3
+ import { Button } from "@reign-labs/ui/button"
4
+ import { Icon } from "@reign-labs/ui/icon"
5
+ import { IconButton } from "@reign-labs/ui/icon-button"
6
+ import { TextField } from "@reign-labs/ui/text-field"
7
+ import { showToast } from "@reign-labs/ui/toast"
8
+ import fuzzysort from "fuzzysort"
9
+ import { formatKeybind, parseKeybind, useCommand } from "@/context/command"
10
+ import { useLanguage } from "@/context/language"
11
+ import { useSettings } from "@/context/settings"
12
+ import { SettingsList } from "./settings-list"
13
+
14
+ const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
15
+ const PALETTE_ID = "command.palette"
16
+ const DEFAULT_PALETTE_KEYBIND = "mod+shift+p"
17
+
18
+ type KeybindGroup = "General" | "Session" | "Navigation" | "Model and agent" | "Terminal" | "Prompt"
19
+
20
+ type KeybindMeta = {
21
+ title: string
22
+ group: KeybindGroup
23
+ }
24
+
25
+ type KeybindMap = Record<string, string | undefined>
26
+ type CommandContext = ReturnType<typeof useCommand>
27
+
28
+ const GROUPS: KeybindGroup[] = ["General", "Session", "Navigation", "Model and agent", "Terminal", "Prompt"]
29
+
30
+ type GroupKey =
31
+ | "settings.shortcuts.group.general"
32
+ | "settings.shortcuts.group.session"
33
+ | "settings.shortcuts.group.navigation"
34
+ | "settings.shortcuts.group.modelAndAgent"
35
+ | "settings.shortcuts.group.terminal"
36
+ | "settings.shortcuts.group.prompt"
37
+
38
+ const groupKey: Record<KeybindGroup, GroupKey> = {
39
+ General: "settings.shortcuts.group.general",
40
+ Session: "settings.shortcuts.group.session",
41
+ Navigation: "settings.shortcuts.group.navigation",
42
+ "Model and agent": "settings.shortcuts.group.modelAndAgent",
43
+ Terminal: "settings.shortcuts.group.terminal",
44
+ Prompt: "settings.shortcuts.group.prompt",
45
+ }
46
+
47
+ function groupFor(id: string): KeybindGroup {
48
+ if (id === PALETTE_ID) return "General"
49
+ if (id.startsWith("terminal.")) return "Terminal"
50
+ if (id.startsWith("model.") || id.startsWith("agent.") || id.startsWith("mcp.")) return "Model and agent"
51
+ if (id.startsWith("file.") || id.startsWith("fileTree.")) return "Navigation"
52
+ if (id.startsWith("prompt.")) return "Prompt"
53
+ if (
54
+ id.startsWith("session.") ||
55
+ id.startsWith("message.") ||
56
+ id.startsWith("permissions.") ||
57
+ id.startsWith("steps.") ||
58
+ id.startsWith("review.")
59
+ )
60
+ return "Session"
61
+
62
+ return "General"
63
+ }
64
+
65
+ function isModifier(key: string) {
66
+ return key === "Shift" || key === "Control" || key === "Alt" || key === "Meta"
67
+ }
68
+
69
+ function normalizeKey(key: string) {
70
+ if (key === ",") return "comma"
71
+ if (key === "+") return "plus"
72
+ if (key === " ") return "space"
73
+ return key.toLowerCase()
74
+ }
75
+
76
+ function recordKeybind(event: KeyboardEvent) {
77
+ if (isModifier(event.key)) return
78
+
79
+ const parts: string[] = []
80
+
81
+ const mod = IS_MAC ? event.metaKey : event.ctrlKey
82
+ if (mod) parts.push("mod")
83
+
84
+ if (IS_MAC && event.ctrlKey) parts.push("ctrl")
85
+ if (!IS_MAC && event.metaKey) parts.push("meta")
86
+ if (event.altKey) parts.push("alt")
87
+ if (event.shiftKey) parts.push("shift")
88
+
89
+ const key = normalizeKey(event.key)
90
+ if (!key) return
91
+ parts.push(key)
92
+
93
+ return parts.join("+")
94
+ }
95
+
96
+ function signatures(config: string | undefined) {
97
+ if (!config) return []
98
+ const sigs: string[] = []
99
+
100
+ for (const kb of parseKeybind(config)) {
101
+ const parts: string[] = []
102
+ if (kb.ctrl) parts.push("ctrl")
103
+ if (kb.alt) parts.push("alt")
104
+ if (kb.shift) parts.push("shift")
105
+ if (kb.meta) parts.push("meta")
106
+ if (kb.key) parts.push(kb.key)
107
+ if (parts.length === 0) continue
108
+ sigs.push(parts.join("+"))
109
+ }
110
+
111
+ return sigs
112
+ }
113
+
114
+ function keybinds(value: unknown): KeybindMap {
115
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {}
116
+ return value as KeybindMap
117
+ }
118
+
119
+ function listFor(command: CommandContext, map: KeybindMap, palette: string) {
120
+ const out = new Map<string, KeybindMeta>()
121
+ out.set(PALETTE_ID, { title: palette, group: "General" })
122
+
123
+ for (const opt of command.catalog) {
124
+ if (opt.id.startsWith("suggested.")) continue
125
+ out.set(opt.id, { title: opt.title, group: groupFor(opt.id) })
126
+ }
127
+
128
+ for (const opt of command.options) {
129
+ if (opt.id.startsWith("suggested.")) continue
130
+ out.set(opt.id, { title: opt.title, group: groupFor(opt.id) })
131
+ }
132
+
133
+ for (const [id, value] of Object.entries(map)) {
134
+ if (typeof value !== "string") continue
135
+ if (out.has(id)) continue
136
+ out.set(id, { title: id, group: groupFor(id) })
137
+ }
138
+
139
+ return out
140
+ }
141
+
142
+ function groupedFor(list: Map<string, KeybindMeta>) {
143
+ const out = new Map<KeybindGroup, string[]>()
144
+ for (const group of GROUPS) out.set(group, [])
145
+
146
+ for (const [id, item] of list) {
147
+ const ids = out.get(item.group)
148
+ if (!ids) continue
149
+ ids.push(id)
150
+ }
151
+
152
+ for (const group of GROUPS) {
153
+ const ids = out.get(group)
154
+ if (!ids) continue
155
+ ids.sort((a, b) => (list.get(a)?.title ?? "").localeCompare(list.get(b)?.title ?? ""))
156
+ }
157
+
158
+ return out
159
+ }
160
+
161
+ function filteredFor(
162
+ query: string,
163
+ list: Map<string, KeybindMeta>,
164
+ grouped: Map<KeybindGroup, string[]>,
165
+ keybind: (id: string) => string,
166
+ ) {
167
+ const value = query.toLowerCase().trim()
168
+ if (!value) return grouped
169
+
170
+ const out = new Map<KeybindGroup, string[]>()
171
+ for (const group of GROUPS) out.set(group, [])
172
+
173
+ const items = Array.from(list.entries()).map(([id, meta]) => ({
174
+ id,
175
+ title: meta.title,
176
+ group: meta.group,
177
+ keybind: keybind(id),
178
+ }))
179
+
180
+ const results = fuzzysort.go(value, items, {
181
+ keys: ["title", "keybind"],
182
+ threshold: -10000,
183
+ })
184
+
185
+ for (const result of results) {
186
+ const ids = out.get(result.obj.group)
187
+ if (!ids) continue
188
+ ids.push(result.obj.id)
189
+ }
190
+
191
+ return out
192
+ }
193
+
194
+ function useKeyCapture(input: {
195
+ active: () => string | null
196
+ stop: () => void
197
+ set: (id: string, keybind: string) => void
198
+ used: () => Map<string, { id: string; title: string }[]>
199
+ language: ReturnType<typeof useLanguage>
200
+ }) {
201
+ onMount(() => {
202
+ const handle = (event: KeyboardEvent) => {
203
+ const id = input.active()
204
+ if (!id) return
205
+
206
+ event.preventDefault()
207
+ event.stopPropagation()
208
+ event.stopImmediatePropagation()
209
+
210
+ if (event.key === "Escape") {
211
+ input.stop()
212
+ return
213
+ }
214
+
215
+ const clear =
216
+ (event.key === "Backspace" || event.key === "Delete") &&
217
+ !event.ctrlKey &&
218
+ !event.metaKey &&
219
+ !event.altKey &&
220
+ !event.shiftKey
221
+ if (clear) {
222
+ input.set(id, "none")
223
+ input.stop()
224
+ return
225
+ }
226
+
227
+ const next = recordKeybind(event)
228
+ if (!next) return
229
+
230
+ const conflicts = new Map<string, string>()
231
+ for (const sig of signatures(next)) {
232
+ for (const item of input.used().get(sig) ?? []) {
233
+ if (item.id === id) continue
234
+ conflicts.set(item.id, item.title)
235
+ }
236
+ }
237
+
238
+ if (conflicts.size > 0) {
239
+ showToast({
240
+ title: input.language.t("settings.shortcuts.conflict.title"),
241
+ description: input.language.t("settings.shortcuts.conflict.description", {
242
+ keybind: formatKeybind(next, input.language.t),
243
+ titles: [...conflicts.values()].join(", "),
244
+ }),
245
+ })
246
+ return
247
+ }
248
+
249
+ input.set(id, next)
250
+ input.stop()
251
+ }
252
+
253
+ document.addEventListener("keydown", handle, true)
254
+ onCleanup(() => document.removeEventListener("keydown", handle, true))
255
+ })
256
+ }
257
+
258
+ export const SettingsKeybinds: Component = () => {
259
+ const command = useCommand()
260
+ const language = useLanguage()
261
+ const settings = useSettings()
262
+
263
+ const [store, setStore] = createStore({
264
+ active: null as string | null,
265
+ filter: "",
266
+ })
267
+
268
+ const stop = () => {
269
+ if (!store.active) return
270
+ setStore("active", null)
271
+ command.keybinds(true)
272
+ }
273
+
274
+ const start = (id: string) => {
275
+ if (store.active === id) {
276
+ stop()
277
+ return
278
+ }
279
+
280
+ if (store.active) stop()
281
+
282
+ setStore("active", id)
283
+ command.keybinds(false)
284
+ }
285
+
286
+ const map = createMemo(() => keybinds(settings.current.keybinds))
287
+
288
+ const hasOverrides = createMemo(() => Object.values(map()).some((x) => typeof x === "string"))
289
+
290
+ const resetAll = () => {
291
+ stop()
292
+ settings.keybinds.resetAll()
293
+ showToast({
294
+ title: language.t("settings.shortcuts.reset.toast.title"),
295
+ description: language.t("settings.shortcuts.reset.toast.description"),
296
+ })
297
+ }
298
+
299
+ const list = createMemo(() => {
300
+ language.locale()
301
+ return listFor(command, map(), language.t("command.palette"))
302
+ })
303
+
304
+ const title = (id: string) => list().get(id)?.title ?? ""
305
+
306
+ const grouped = createMemo(() => groupedFor(list()))
307
+
308
+ const filtered = createMemo(() => {
309
+ return filteredFor(store.filter, list(), grouped(), (id) => command.keybind(id) || "")
310
+ })
311
+
312
+ const hasResults = createMemo(() => {
313
+ for (const group of GROUPS) {
314
+ const ids = filtered().get(group) ?? []
315
+ if (ids.length > 0) return true
316
+ }
317
+ return false
318
+ })
319
+
320
+ const used = createMemo(() => {
321
+ const map = new Map<string, { id: string; title: string }[]>()
322
+
323
+ const add = (key: string, value: { id: string; title: string }) => {
324
+ const list = map.get(key)
325
+ if (!list) {
326
+ map.set(key, [value])
327
+ return
328
+ }
329
+ list.push(value)
330
+ }
331
+
332
+ const palette = settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND
333
+ for (const sig of signatures(palette)) {
334
+ add(sig, { id: PALETTE_ID, title: title(PALETTE_ID) })
335
+ }
336
+
337
+ const valueFor = (id: string) => {
338
+ const custom = settings.keybinds.get(id)
339
+ if (typeof custom === "string") return custom
340
+
341
+ const live = command.options.find((x) => x.id === id)
342
+ if (live?.keybind) return live.keybind
343
+
344
+ const meta = command.catalog.find((x) => x.id === id)
345
+ return meta?.keybind
346
+ }
347
+
348
+ for (const id of list().keys()) {
349
+ if (id === PALETTE_ID) continue
350
+ for (const sig of signatures(valueFor(id))) {
351
+ add(sig, { id, title: title(id) })
352
+ }
353
+ }
354
+
355
+ return map
356
+ })
357
+
358
+ const setKeybind = (id: string, keybind: string) => settings.keybinds.set(id, keybind)
359
+
360
+ useKeyCapture({
361
+ active: () => store.active,
362
+ stop,
363
+ set: setKeybind,
364
+ used,
365
+ language,
366
+ })
367
+
368
+ onCleanup(() => {
369
+ if (store.active) command.keybinds(true)
370
+ })
371
+
372
+ return (
373
+ <div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
374
+ <div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
375
+ <div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
376
+ <div class="flex items-center justify-between gap-4">
377
+ <h2 class="text-16-medium text-text-strong">{language.t("settings.shortcuts.title")}</h2>
378
+ <Button size="small" variant="secondary" onClick={resetAll} disabled={!hasOverrides()}>
379
+ {language.t("settings.shortcuts.reset.button")}
380
+ </Button>
381
+ </div>
382
+
383
+ <div class="flex items-center gap-2 px-3 h-9 rounded-lg bg-surface-base">
384
+ <Icon name="magnifying-glass" class="text-icon-weak-base flex-shrink-0" />
385
+ <TextField
386
+ variant="ghost"
387
+ type="text"
388
+ value={store.filter}
389
+ onChange={(v) => setStore("filter", v)}
390
+ placeholder={language.t("settings.shortcuts.search.placeholder")}
391
+ spellcheck={false}
392
+ autocorrect="off"
393
+ autocomplete="off"
394
+ autocapitalize="off"
395
+ class="flex-1"
396
+ />
397
+ <Show when={store.filter}>
398
+ <IconButton icon="circle-x" variant="ghost" onClick={() => setStore("filter", "")} />
399
+ </Show>
400
+ </div>
401
+ </div>
402
+ </div>
403
+
404
+ <div class="flex flex-col gap-8 max-w-[720px]">
405
+ <For each={GROUPS}>
406
+ {(group) => (
407
+ <Show when={(filtered().get(group) ?? []).length > 0}>
408
+ <div class="flex flex-col gap-1">
409
+ <h3 class="text-14-medium text-text-strong pb-2">{language.t(groupKey[group])}</h3>
410
+ <SettingsList>
411
+ <For each={filtered().get(group) ?? []}>
412
+ {(id) => (
413
+ <div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
414
+ <span class="text-14-regular text-text-strong">{title(id)}</span>
415
+ <button
416
+ type="button"
417
+ data-keybind-id={id}
418
+ classList={{
419
+ "h-8 px-3 rounded-md text-12-regular": true,
420
+ "bg-surface-base text-text-subtle hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active":
421
+ store.active !== id,
422
+ "border border-border-weak-base bg-surface-inset-base text-text-weak": store.active === id,
423
+ }}
424
+ onClick={() => start(id)}
425
+ >
426
+ <Show
427
+ when={store.active === id}
428
+ fallback={command.keybind(id) || language.t("settings.shortcuts.unassigned")}
429
+ >
430
+ {language.t("settings.shortcuts.pressKeys")}
431
+ </Show>
432
+ </button>
433
+ </div>
434
+ )}
435
+ </For>
436
+ </SettingsList>
437
+ </div>
438
+ </Show>
439
+ )}
440
+ </For>
441
+
442
+ <Show when={store.filter && !hasResults()}>
443
+ <div class="flex flex-col items-center justify-center py-12 text-center">
444
+ <span class="text-14-regular text-text-weak">{language.t("settings.shortcuts.search.empty")}</span>
445
+ <Show when={store.filter}>
446
+ <span class="text-14-regular text-text-strong mt-1">"{store.filter}"</span>
447
+ </Show>
448
+ </div>
449
+ </Show>
450
+ </div>
451
+ </div>
452
+ )
453
+ }
@@ -0,0 +1,5 @@
1
+ import { type Component, type JSX } from "solid-js"
2
+
3
+ export const SettingsList: Component<{ children: JSX.Element }> = (props) => {
4
+ return <div class="bg-surface-base px-4 rounded-lg">{props.children}</div>
5
+ }
@@ -0,0 +1,137 @@
1
+ import { useFilteredList } from "@reign-labs/ui/hooks"
2
+ import { ProviderIcon } from "@reign-labs/ui/provider-icon"
3
+ import { Switch } from "@reign-labs/ui/switch"
4
+ import { Icon } from "@reign-labs/ui/icon"
5
+ import { IconButton } from "@reign-labs/ui/icon-button"
6
+ import { TextField } from "@reign-labs/ui/text-field"
7
+ import { type Component, For, Show } from "solid-js"
8
+ import { useLanguage } from "@/context/language"
9
+ import { useModels } from "@/context/models"
10
+ import { popularProviders } from "@/hooks/use-providers"
11
+ import { SettingsList } from "./settings-list"
12
+
13
+ type ModelItem = ReturnType<ReturnType<typeof useModels>["list"]>[number]
14
+
15
+ const ListLoadingState: Component<{ label: string }> = (props) => {
16
+ return (
17
+ <div class="flex flex-col items-center justify-center py-12 text-center">
18
+ <span class="text-14-regular text-text-weak">{props.label}</span>
19
+ </div>
20
+ )
21
+ }
22
+
23
+ const ListEmptyState: Component<{ message: string; filter: string }> = (props) => {
24
+ return (
25
+ <div class="flex flex-col items-center justify-center py-12 text-center">
26
+ <span class="text-14-regular text-text-weak">{props.message}</span>
27
+ <Show when={props.filter}>
28
+ <span class="text-14-regular text-text-strong mt-1">&quot;{props.filter}&quot;</span>
29
+ </Show>
30
+ </div>
31
+ )
32
+ }
33
+
34
+ export const SettingsModels: Component = () => {
35
+ const language = useLanguage()
36
+ const models = useModels()
37
+
38
+ const list = useFilteredList<ModelItem>({
39
+ items: (_filter) => models.list(),
40
+ key: (x) => `${x.provider.id}:${x.id}`,
41
+ filterKeys: ["provider.name", "name", "id"],
42
+ sortBy: (a, b) => a.name.localeCompare(b.name),
43
+ groupBy: (x) => x.provider.id,
44
+ sortGroupsBy: (a, b) => {
45
+ const aIndex = popularProviders.indexOf(a.category)
46
+ const bIndex = popularProviders.indexOf(b.category)
47
+ const aPopular = aIndex >= 0
48
+ const bPopular = bIndex >= 0
49
+
50
+ if (aPopular && !bPopular) return -1
51
+ if (!aPopular && bPopular) return 1
52
+ if (aPopular && bPopular) return aIndex - bIndex
53
+
54
+ const aName = a.items[0].provider.name
55
+ const bName = b.items[0].provider.name
56
+ return aName.localeCompare(bName)
57
+ },
58
+ })
59
+
60
+ return (
61
+ <div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
62
+ <div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
63
+ <div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
64
+ <h2 class="text-16-medium text-text-strong">{language.t("settings.models.title")}</h2>
65
+ <div class="flex items-center gap-2 px-3 h-9 rounded-lg bg-surface-base">
66
+ <Icon name="magnifying-glass" class="text-icon-weak-base flex-shrink-0" />
67
+ <TextField
68
+ variant="ghost"
69
+ type="text"
70
+ value={list.filter()}
71
+ onChange={list.onInput}
72
+ placeholder={language.t("dialog.model.search.placeholder")}
73
+ spellcheck={false}
74
+ autocorrect="off"
75
+ autocomplete="off"
76
+ autocapitalize="off"
77
+ class="flex-1"
78
+ />
79
+ <Show when={list.filter()}>
80
+ <IconButton icon="circle-x" variant="ghost" onClick={list.clear} />
81
+ </Show>
82
+ </div>
83
+ </div>
84
+ </div>
85
+
86
+ <div class="flex flex-col gap-8 max-w-[720px]">
87
+ <Show
88
+ when={!list.grouped.loading}
89
+ fallback={
90
+ <ListLoadingState label={`${language.t("common.loading")}${language.t("common.loading.ellipsis")}`} />
91
+ }
92
+ >
93
+ <Show
94
+ when={list.flat().length > 0}
95
+ fallback={<ListEmptyState message={language.t("dialog.model.empty")} filter={list.filter()} />}
96
+ >
97
+ <For each={list.grouped.latest}>
98
+ {(group) => (
99
+ <div class="flex flex-col gap-1">
100
+ <div class="flex items-center gap-2 pb-2">
101
+ <ProviderIcon id={group.category} class="size-5 shrink-0 icon-strong-base" />
102
+ <span class="text-14-medium text-text-strong">{group.items[0].provider.name}</span>
103
+ </div>
104
+ <SettingsList>
105
+ <For each={group.items}>
106
+ {(item) => {
107
+ const key = { providerID: item.provider.id, modelID: item.id }
108
+ return (
109
+ <div class="flex flex-wrap items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
110
+ <div class="min-w-0">
111
+ <span class="text-14-regular text-text-strong truncate block">{item.name}</span>
112
+ </div>
113
+ <div class="flex-shrink-0">
114
+ <Switch
115
+ checked={models.visible(key)}
116
+ onChange={(checked) => {
117
+ models.setVisibility(key, checked)
118
+ }}
119
+ hideLabel
120
+ >
121
+ {item.name}
122
+ </Switch>
123
+ </div>
124
+ </div>
125
+ )
126
+ }}
127
+ </For>
128
+ </SettingsList>
129
+ </div>
130
+ )}
131
+ </For>
132
+ </Show>
133
+ </Show>
134
+ </div>
135
+ </div>
136
+ )
137
+ }