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,255 @@
1
+ import { Button } from "@reign-labs/ui/button"
2
+ import { useDialog } from "@reign-labs/ui/context/dialog"
3
+ import { Dialog } from "@reign-labs/ui/dialog"
4
+ import { TextField } from "@reign-labs/ui/text-field"
5
+ import { useMutation } from "@tanstack/solid-query"
6
+ import { Icon } from "@reign-labs/ui/icon"
7
+ import { createMemo, For, Show } from "solid-js"
8
+ import { createStore } from "solid-js/store"
9
+ import { useGlobalSDK } from "@/context/global-sdk"
10
+ import { useGlobalSync } from "@/context/global-sync"
11
+ import { type LocalProject, getAvatarColors } from "@/context/layout"
12
+ import { getFilename } from "@reign-labs/util/path"
13
+ import { Avatar } from "@reign-labs/ui/avatar"
14
+ import { useLanguage } from "@/context/language"
15
+
16
+ const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
17
+
18
+ export function DialogEditProject(props: { project: LocalProject }) {
19
+ const dialog = useDialog()
20
+ const globalSDK = useGlobalSDK()
21
+ const globalSync = useGlobalSync()
22
+ const language = useLanguage()
23
+
24
+ const folderName = createMemo(() => getFilename(props.project.worktree))
25
+ const defaultName = createMemo(() => props.project.name || folderName())
26
+
27
+ const [store, setStore] = createStore({
28
+ name: defaultName(),
29
+ color: props.project.icon?.color || "pink",
30
+ iconUrl: props.project.icon?.override || "",
31
+ startup: props.project.commands?.start ?? "",
32
+ dragOver: false,
33
+ iconHover: false,
34
+ })
35
+
36
+ let iconInput: HTMLInputElement | undefined
37
+
38
+ function handleFileSelect(file: File) {
39
+ if (!file.type.startsWith("image/")) return
40
+ const reader = new FileReader()
41
+ reader.onload = (e) => {
42
+ setStore("iconUrl", e.target?.result as string)
43
+ setStore("iconHover", false)
44
+ }
45
+ reader.readAsDataURL(file)
46
+ }
47
+
48
+ function handleDrop(e: DragEvent) {
49
+ e.preventDefault()
50
+ setStore("dragOver", false)
51
+ const file = e.dataTransfer?.files[0]
52
+ if (file) handleFileSelect(file)
53
+ }
54
+
55
+ function handleDragOver(e: DragEvent) {
56
+ e.preventDefault()
57
+ setStore("dragOver", true)
58
+ }
59
+
60
+ function handleDragLeave() {
61
+ setStore("dragOver", false)
62
+ }
63
+
64
+ function handleInputChange(e: Event) {
65
+ const input = e.target as HTMLInputElement
66
+ const file = input.files?.[0]
67
+ if (file) handleFileSelect(file)
68
+ }
69
+
70
+ function clearIcon() {
71
+ setStore("iconUrl", "")
72
+ }
73
+
74
+ const saveMutation = useMutation(() => ({
75
+ mutationFn: async () => {
76
+ const name = store.name.trim() === folderName() ? "" : store.name.trim()
77
+ const start = store.startup.trim()
78
+
79
+ if (props.project.id && props.project.id !== "global") {
80
+ await globalSDK.client.project.update({
81
+ projectID: props.project.id,
82
+ directory: props.project.worktree,
83
+ name,
84
+ icon: { color: store.color, override: store.iconUrl },
85
+ commands: { start },
86
+ })
87
+ globalSync.project.icon(props.project.worktree, store.iconUrl || undefined)
88
+ dialog.close()
89
+ return
90
+ }
91
+
92
+ globalSync.project.meta(props.project.worktree, {
93
+ name,
94
+ icon: { color: store.color, override: store.iconUrl || undefined },
95
+ commands: { start: start || undefined },
96
+ })
97
+ dialog.close()
98
+ },
99
+ }))
100
+
101
+ function handleSubmit(e: SubmitEvent) {
102
+ e.preventDefault()
103
+ if (saveMutation.isPending) return
104
+ saveMutation.mutate()
105
+ }
106
+
107
+ return (
108
+ <Dialog title={language.t("dialog.project.edit.title")} class="w-full max-w-[480px] mx-auto">
109
+ <form onSubmit={handleSubmit} class="flex flex-col gap-6 p-6 pt-0">
110
+ <div class="flex flex-col gap-4">
111
+ <TextField
112
+ autofocus
113
+ type="text"
114
+ label={language.t("dialog.project.edit.name")}
115
+ placeholder={folderName()}
116
+ value={store.name}
117
+ onChange={(v) => setStore("name", v)}
118
+ />
119
+
120
+ <div class="flex flex-col gap-2">
121
+ <label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.icon")}</label>
122
+ <div class="flex gap-3 items-start">
123
+ <div
124
+ class="relative"
125
+ onMouseEnter={() => setStore("iconHover", true)}
126
+ onMouseLeave={() => setStore("iconHover", false)}
127
+ >
128
+ <div
129
+ class="relative size-16 rounded-md transition-colors cursor-pointer"
130
+ classList={{
131
+ "border-text-interactive-base bg-surface-info-base/20": store.dragOver,
132
+ "border-border-base hover:border-border-strong": !store.dragOver,
133
+ "overflow-hidden": !!store.iconUrl,
134
+ }}
135
+ onDrop={handleDrop}
136
+ onDragOver={handleDragOver}
137
+ onDragLeave={handleDragLeave}
138
+ onClick={() => {
139
+ if (store.iconUrl && store.iconHover) {
140
+ clearIcon()
141
+ } else {
142
+ iconInput?.click()
143
+ }
144
+ }}
145
+ >
146
+ <Show
147
+ when={store.iconUrl}
148
+ fallback={
149
+ <div class="size-full flex items-center justify-center">
150
+ <Avatar
151
+ fallback={store.name || defaultName()}
152
+ {...getAvatarColors(store.color)}
153
+ class="size-full text-[32px]"
154
+ />
155
+ </div>
156
+ }
157
+ >
158
+ <img
159
+ src={store.iconUrl}
160
+ alt={language.t("dialog.project.edit.icon.alt")}
161
+ class="size-full object-cover"
162
+ />
163
+ </Show>
164
+ </div>
165
+ <div
166
+ class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
167
+ classList={{
168
+ "opacity-100": store.iconHover && !store.iconUrl,
169
+ "opacity-0": !(store.iconHover && !store.iconUrl),
170
+ }}
171
+ >
172
+ <Icon name="cloud-upload" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
173
+ </div>
174
+ <div
175
+ class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
176
+ classList={{
177
+ "opacity-100": store.iconHover && !!store.iconUrl,
178
+ "opacity-0": !(store.iconHover && !!store.iconUrl),
179
+ }}
180
+ >
181
+ <Icon name="trash" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
182
+ </div>
183
+ </div>
184
+ <input
185
+ id="icon-upload"
186
+ ref={(el) => {
187
+ iconInput = el
188
+ }}
189
+ type="file"
190
+ accept="image/*"
191
+ class="hidden"
192
+ onChange={handleInputChange}
193
+ />
194
+ <div class="flex flex-col gap-1.5 text-12-regular text-text-weak self-center">
195
+ <span>{language.t("dialog.project.edit.icon.hint")}</span>
196
+ <span>{language.t("dialog.project.edit.icon.recommended")}</span>
197
+ </div>
198
+ </div>
199
+ </div>
200
+
201
+ <Show when={!store.iconUrl}>
202
+ <div class="flex flex-col gap-2">
203
+ <label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.color")}</label>
204
+ <div class="flex gap-1.5">
205
+ <For each={AVATAR_COLOR_KEYS}>
206
+ {(color) => (
207
+ <button
208
+ type="button"
209
+ aria-label={language.t("dialog.project.edit.color.select", { color })}
210
+ aria-pressed={store.color === color}
211
+ classList={{
212
+ "flex items-center justify-center size-10 p-0.5 rounded-lg overflow-hidden transition-colors cursor-default": true,
213
+ "bg-transparent border-2 border-icon-strong-base hover:bg-surface-base-hover":
214
+ store.color === color,
215
+ "bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base":
216
+ store.color !== color,
217
+ }}
218
+ onClick={() => setStore("color", color)}
219
+ >
220
+ <Avatar
221
+ fallback={store.name || defaultName()}
222
+ {...getAvatarColors(color)}
223
+ class="size-full rounded"
224
+ />
225
+ </button>
226
+ )}
227
+ </For>
228
+ </div>
229
+ </div>
230
+ </Show>
231
+
232
+ <TextField
233
+ multiline
234
+ label={language.t("dialog.project.edit.worktree.startup")}
235
+ description={language.t("dialog.project.edit.worktree.startup.description")}
236
+ placeholder={language.t("dialog.project.edit.worktree.startup.placeholder")}
237
+ value={store.startup}
238
+ onChange={(v) => setStore("startup", v)}
239
+ spellcheck={false}
240
+ class="max-h-14 w-full overflow-y-auto font-mono text-xs"
241
+ />
242
+ </div>
243
+
244
+ <div class="flex justify-end gap-2">
245
+ <Button type="button" variant="ghost" size="large" onClick={() => dialog.close()}>
246
+ {language.t("common.cancel")}
247
+ </Button>
248
+ <Button type="submit" variant="primary" size="large" disabled={saveMutation.isPending}>
249
+ {saveMutation.isPending ? language.t("common.saving") : language.t("common.save")}
250
+ </Button>
251
+ </div>
252
+ </form>
253
+ </Dialog>
254
+ )
255
+ }
@@ -0,0 +1,108 @@
1
+ import { Component, createMemo } from "solid-js"
2
+ import { useNavigate, useParams } from "@solidjs/router"
3
+ import { useSync } from "@/context/sync"
4
+ import { useSDK } from "@/context/sdk"
5
+ import { usePrompt } from "@/context/prompt"
6
+ import { useDialog } from "@reign-labs/ui/context/dialog"
7
+ import { Dialog } from "@reign-labs/ui/dialog"
8
+ import { List } from "@reign-labs/ui/list"
9
+ import { showToast } from "@reign-labs/ui/toast"
10
+ import { extractPromptFromParts } from "@/utils/prompt"
11
+ import type { TextPart as SDKTextPart } from "@reign-labs/sdk/v2/client"
12
+ import { base64Encode } from "@reign-labs/util/encode"
13
+ import { useLanguage } from "@/context/language"
14
+
15
+ interface ForkableMessage {
16
+ id: string
17
+ text: string
18
+ time: string
19
+ }
20
+
21
+ function formatTime(date: Date): string {
22
+ return date.toLocaleTimeString(undefined, { timeStyle: "short" })
23
+ }
24
+
25
+ export const DialogFork: Component = () => {
26
+ const params = useParams()
27
+ const navigate = useNavigate()
28
+ const sync = useSync()
29
+ const sdk = useSDK()
30
+ const prompt = usePrompt()
31
+ const dialog = useDialog()
32
+ const language = useLanguage()
33
+
34
+ const messages = createMemo((): ForkableMessage[] => {
35
+ const sessionID = params.id
36
+ if (!sessionID) return []
37
+
38
+ const msgs = sync.data.message[sessionID] ?? []
39
+ const result: ForkableMessage[] = []
40
+
41
+ for (const message of msgs) {
42
+ if (message.role !== "user") continue
43
+
44
+ const parts = sync.data.part[message.id] ?? []
45
+ const textPart = parts.find((x): x is SDKTextPart => x.type === "text" && !x.synthetic && !x.ignored)
46
+ if (!textPart) continue
47
+
48
+ result.push({
49
+ id: message.id,
50
+ text: textPart.text.replace(/\n/g, " ").slice(0, 200),
51
+ time: formatTime(new Date(message.time.created)),
52
+ })
53
+ }
54
+
55
+ return result.reverse()
56
+ })
57
+
58
+ const handleSelect = (item: ForkableMessage | undefined) => {
59
+ if (!item) return
60
+
61
+ const sessionID = params.id
62
+ if (!sessionID) return
63
+
64
+ const parts = sync.data.part[item.id] ?? []
65
+ const restored = extractPromptFromParts(parts, {
66
+ directory: sdk.directory,
67
+ attachmentName: language.t("common.attachment"),
68
+ })
69
+ const dir = base64Encode(sdk.directory)
70
+
71
+ sdk.client.session
72
+ .fork({ sessionID, messageID: item.id })
73
+ .then((forked) => {
74
+ if (!forked.data) {
75
+ showToast({ title: language.t("common.requestFailed") })
76
+ return
77
+ }
78
+ dialog.close()
79
+ prompt.set(restored, undefined, { dir, id: forked.data.id })
80
+ navigate(`/${dir}/session/${forked.data.id}`)
81
+ })
82
+ .catch((err: unknown) => {
83
+ const message = err instanceof Error ? err.message : String(err)
84
+ showToast({ title: language.t("common.requestFailed"), description: message })
85
+ })
86
+ }
87
+
88
+ return (
89
+ <Dialog title={language.t("command.session.fork")}>
90
+ <List
91
+ class="flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0"
92
+ search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }}
93
+ emptyMessage={language.t("dialog.fork.empty")}
94
+ key={(x) => x.id}
95
+ items={messages}
96
+ filterKeys={["text"]}
97
+ onSelect={handleSelect}
98
+ >
99
+ {(item) => (
100
+ <div class="w-full flex items-center gap-2">
101
+ <span class="truncate flex-1 min-w-0 text-left font-normal">{item.text}</span>
102
+ <span class="text-text-weak shrink-0 font-normal">{item.time}</span>
103
+ </div>
104
+ )}
105
+ </List>
106
+ </Dialog>
107
+ )
108
+ }
@@ -0,0 +1,101 @@
1
+ import { Dialog } from "@reign-labs/ui/dialog"
2
+ import { List } from "@reign-labs/ui/list"
3
+ import { Switch } from "@reign-labs/ui/switch"
4
+ import { Tooltip } from "@reign-labs/ui/tooltip"
5
+ import { Button } from "@reign-labs/ui/button"
6
+ import type { Component } from "solid-js"
7
+ import { useLocal } from "@/context/local"
8
+ import { popularProviders } from "@/hooks/use-providers"
9
+ import { useLanguage } from "@/context/language"
10
+ import { useDialog } from "@reign-labs/ui/context/dialog"
11
+ import { DialogSelectProvider } from "./dialog-select-provider"
12
+
13
+ export const DialogManageModels: Component = () => {
14
+ const local = useLocal()
15
+ const language = useLanguage()
16
+ const dialog = useDialog()
17
+
18
+ const handleConnectProvider = () => {
19
+ dialog.show(() => <DialogSelectProvider />)
20
+ }
21
+ const providerRank = (id: string) => popularProviders.indexOf(id)
22
+ const providerList = (providerID: string) => local.model.list().filter((x) => x.provider.id === providerID)
23
+ const providerVisible = (providerID: string) =>
24
+ providerList(providerID).every((x) => local.model.visible({ modelID: x.id, providerID: x.provider.id }))
25
+ const setProviderVisibility = (providerID: string, checked: boolean) => {
26
+ providerList(providerID).forEach((x) => {
27
+ local.model.setVisibility({ modelID: x.id, providerID: x.provider.id }, checked)
28
+ })
29
+ }
30
+
31
+ return (
32
+ <Dialog
33
+ title={language.t("dialog.model.manage")}
34
+ description={language.t("dialog.model.manage.description")}
35
+ action={
36
+ <Button class="h-7 -my-1 text-14-medium" icon="plus-small" tabIndex={-1} onClick={handleConnectProvider}>
37
+ {language.t("command.provider.connect")}
38
+ </Button>
39
+ }
40
+ >
41
+ <List
42
+ search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true }}
43
+ emptyMessage={language.t("dialog.model.empty")}
44
+ key={(x) => `${x?.provider?.id}:${x?.id}`}
45
+ items={local.model.list()}
46
+ filterKeys={["provider.name", "name", "id"]}
47
+ sortBy={(a, b) => a.name.localeCompare(b.name)}
48
+ groupBy={(x) => x.provider.id}
49
+ groupHeader={(group) => {
50
+ const provider = group.items[0].provider
51
+ return (
52
+ <>
53
+ <span>{provider.name}</span>
54
+ <Tooltip
55
+ placement="top"
56
+ value={language.t("dialog.model.manage.provider.toggle", { provider: provider.name })}
57
+ >
58
+ <Switch
59
+ class="-mr-1"
60
+ checked={providerVisible(provider.id)}
61
+ onChange={(checked) => setProviderVisibility(provider.id, checked)}
62
+ hideLabel
63
+ >
64
+ {provider.name}
65
+ </Switch>
66
+ </Tooltip>
67
+ </>
68
+ )
69
+ }}
70
+ sortGroupsBy={(a, b) => {
71
+ const aRank = providerRank(a.items[0].provider.id)
72
+ const bRank = providerRank(b.items[0].provider.id)
73
+ const aPopular = aRank >= 0
74
+ const bPopular = bRank >= 0
75
+ if (aPopular && !bPopular) return -1
76
+ if (!aPopular && bPopular) return 1
77
+ return aRank - bRank
78
+ }}
79
+ onSelect={(x) => {
80
+ if (!x) return
81
+ const key = { modelID: x.id, providerID: x.provider.id }
82
+ local.model.setVisibility(key, !local.model.visible(key))
83
+ }}
84
+ >
85
+ {(i) => (
86
+ <div class="w-full flex items-center justify-between gap-x-3">
87
+ <span>{i.name}</span>
88
+ <div onClick={(e) => e.stopPropagation()}>
89
+ <Switch
90
+ checked={!!local.model.visible({ modelID: i.id, providerID: i.provider.id })}
91
+ onChange={(checked) => {
92
+ local.model.setVisibility({ modelID: i.id, providerID: i.provider.id }, checked)
93
+ }}
94
+ />
95
+ </div>
96
+ </div>
97
+ )}
98
+ </List>
99
+ </Dialog>
100
+ )
101
+ }
@@ -0,0 +1,144 @@
1
+ import { createSignal } from "solid-js"
2
+ import { Dialog } from "@reign-labs/ui/dialog"
3
+ import { Button } from "@reign-labs/ui/button"
4
+ import { useDialog } from "@reign-labs/ui/context/dialog"
5
+ import { useLanguage } from "@/context/language"
6
+ import { useSettings } from "@/context/settings"
7
+
8
+ export type Highlight = {
9
+ title: string
10
+ description: string
11
+ media?: {
12
+ type: "image" | "video"
13
+ src: string
14
+ alt?: string
15
+ }
16
+ }
17
+
18
+ export function DialogReleaseNotes(props: { highlights: Highlight[] }) {
19
+ const dialog = useDialog()
20
+ const language = useLanguage()
21
+ const settings = useSettings()
22
+ const [index, setIndex] = createSignal(0)
23
+
24
+ const total = () => props.highlights.length
25
+ const last = () => Math.max(0, total() - 1)
26
+ const feature = () => props.highlights[index()] ?? props.highlights[last()]
27
+ const isFirst = () => index() === 0
28
+ const isLast = () => index() >= last()
29
+ const paged = () => total() > 1
30
+
31
+ function handleNext() {
32
+ if (isLast()) return
33
+ setIndex(index() + 1)
34
+ }
35
+
36
+ function handleClose() {
37
+ dialog.close()
38
+ }
39
+
40
+ function handleDisable() {
41
+ settings.general.setReleaseNotes(false)
42
+ handleClose()
43
+ }
44
+
45
+ function handleKeyDown(e: KeyboardEvent) {
46
+ if (e.key === "Escape") {
47
+ e.preventDefault()
48
+ handleClose()
49
+ return
50
+ }
51
+
52
+ if (!paged()) return
53
+ if (e.key === "ArrowLeft" && !isFirst()) {
54
+ e.preventDefault()
55
+ setIndex(index() - 1)
56
+ }
57
+ if (e.key === "ArrowRight" && !isLast()) {
58
+ e.preventDefault()
59
+ setIndex(index() + 1)
60
+ }
61
+ }
62
+
63
+ return (
64
+ <Dialog
65
+ size="large"
66
+ fit
67
+ class="w-[min(calc(100vw-40px),720px)] h-[min(calc(100vh-40px),400px)] -mt-20 min-h-0 overflow-hidden"
68
+ >
69
+ <div class="flex flex-1 min-w-0 min-h-0" tabIndex={0} autofocus onKeyDown={handleKeyDown}>
70
+ {/* Left side - Text content */}
71
+ <div class="flex flex-col flex-1 min-w-0 p-8">
72
+ {/* Top section - feature content (fixed position from top) */}
73
+ <div class="flex flex-col gap-2 pt-22">
74
+ <div class="flex items-center gap-2">
75
+ <h1 class="text-16-medium text-text-strong">{feature()?.title ?? ""}</h1>
76
+ </div>
77
+ <p class="text-14-regular text-text-base">{feature()?.description ?? ""}</p>
78
+ </div>
79
+
80
+ {/* Spacer to push buttons to bottom */}
81
+ <div class="flex-1" />
82
+
83
+ {/* Bottom section - buttons and indicators (fixed position) */}
84
+ <div class="flex flex-col gap-12">
85
+ <div class="flex flex-col items-start gap-3">
86
+ {isLast() ? (
87
+ <Button variant="primary" size="large" onClick={handleClose}>
88
+ {language.t("dialog.releaseNotes.action.getStarted")}
89
+ </Button>
90
+ ) : (
91
+ <Button variant="secondary" size="large" onClick={handleNext}>
92
+ {language.t("dialog.releaseNotes.action.next")}
93
+ </Button>
94
+ )}
95
+
96
+ <Button variant="ghost" size="small" onClick={handleDisable}>
97
+ {language.t("dialog.releaseNotes.action.hideFuture")}
98
+ </Button>
99
+ </div>
100
+
101
+ {paged() && (
102
+ <div class="flex items-center gap-1.5 -my-2.5">
103
+ {props.highlights.map((_, i) => (
104
+ <button
105
+ type="button"
106
+ class="h-6 flex items-center cursor-pointer bg-transparent border-none p-0 transition-all duration-200"
107
+ classList={{
108
+ "w-8": i === index(),
109
+ "w-3": i !== index(),
110
+ }}
111
+ onClick={() => setIndex(i)}
112
+ >
113
+ <div
114
+ class="w-full h-0.5 rounded-[1px] transition-colors duration-200"
115
+ classList={{
116
+ "bg-icon-strong-base": i === index(),
117
+ "bg-icon-weak-base": i !== index(),
118
+ }}
119
+ />
120
+ </button>
121
+ ))}
122
+ </div>
123
+ )}
124
+ </div>
125
+ </div>
126
+
127
+ {/* Right side - Media content (edge to edge) */}
128
+ {feature()?.media && (
129
+ <div class="flex-1 min-w-0 bg-surface-base overflow-hidden rounded-r-xl">
130
+ {feature()!.media!.type === "image" ? (
131
+ <img
132
+ src={feature()!.media!.src}
133
+ alt={feature()!.media!.alt ?? feature()?.title ?? language.t("dialog.releaseNotes.media.alt")}
134
+ class="w-full h-full object-cover"
135
+ />
136
+ ) : (
137
+ <video src={feature()!.media!.src} autoplay loop muted playsinline class="w-full h-full object-cover" />
138
+ )}
139
+ </div>
140
+ )}
141
+ </div>
142
+ </Dialog>
143
+ )
144
+ }