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,175 @@
1
+ import { getFilename } from "@reign-labs/util/path"
2
+ import { type AgentPartInput, type FilePartInput, type Part, type TextPartInput } from "@reign-labs/sdk/v2/client"
3
+ import type { FileSelection } from "@/context/file"
4
+ import { encodeFilePath } from "@/context/file/path"
5
+ import type { AgentPart, FileAttachmentPart, ImageAttachmentPart, Prompt } from "@/context/prompt"
6
+ import { Identifier } from "@/utils/id"
7
+ import { createCommentMetadata, formatCommentNote } from "@/utils/comment-note"
8
+
9
+ type PromptRequestPart = (TextPartInput | FilePartInput | AgentPartInput) & { id: string }
10
+
11
+ type ContextFile = {
12
+ key: string
13
+ type: "file"
14
+ path: string
15
+ selection?: FileSelection
16
+ comment?: string
17
+ commentID?: string
18
+ commentOrigin?: "review" | "file"
19
+ preview?: string
20
+ }
21
+
22
+ type BuildRequestPartsInput = {
23
+ prompt: Prompt
24
+ context: ContextFile[]
25
+ images: ImageAttachmentPart[]
26
+ text: string
27
+ messageID: string
28
+ sessionID: string
29
+ sessionDirectory: string
30
+ }
31
+
32
+ const absolute = (directory: string, path: string) => {
33
+ if (path.startsWith("/")) return path
34
+ if (/^[A-Za-z]:[\\/]/.test(path) || /^[A-Za-z]:$/.test(path)) return path
35
+ if (path.startsWith("\\\\") || path.startsWith("//")) return path
36
+ return `${directory.replace(/[\\/]+$/, "")}/${path}`
37
+ }
38
+
39
+ const fileQuery = (selection: FileSelection | undefined) =>
40
+ selection ? `?start=${selection.startLine}&end=${selection.endLine}` : ""
41
+
42
+ const isFileAttachment = (part: Prompt[number]): part is FileAttachmentPart => part.type === "file"
43
+ const isAgentAttachment = (part: Prompt[number]): part is AgentPart => part.type === "agent"
44
+
45
+ const toOptimisticPart = (part: PromptRequestPart, sessionID: string, messageID: string): Part => {
46
+ if (part.type === "text") {
47
+ return {
48
+ id: part.id,
49
+ type: "text",
50
+ text: part.text,
51
+ synthetic: part.synthetic,
52
+ ignored: part.ignored,
53
+ time: part.time,
54
+ metadata: part.metadata,
55
+ sessionID,
56
+ messageID,
57
+ }
58
+ }
59
+ if (part.type === "file") {
60
+ return {
61
+ id: part.id,
62
+ type: "file",
63
+ mime: part.mime,
64
+ filename: part.filename,
65
+ url: part.url,
66
+ source: part.source,
67
+ sessionID,
68
+ messageID,
69
+ }
70
+ }
71
+ return {
72
+ id: part.id,
73
+ type: "agent",
74
+ name: part.name,
75
+ source: part.source,
76
+ sessionID,
77
+ messageID,
78
+ }
79
+ }
80
+
81
+ export function buildRequestParts(input: BuildRequestPartsInput) {
82
+ const requestParts: PromptRequestPart[] = [
83
+ {
84
+ id: Identifier.ascending("part"),
85
+ type: "text",
86
+ text: input.text,
87
+ },
88
+ ]
89
+
90
+ const files = input.prompt.filter(isFileAttachment).map((attachment) => {
91
+ const path = absolute(input.sessionDirectory, attachment.path)
92
+ return {
93
+ id: Identifier.ascending("part"),
94
+ type: "file",
95
+ mime: "text/plain",
96
+ url: `file://${encodeFilePath(path)}${fileQuery(attachment.selection)}`,
97
+ filename: getFilename(attachment.path),
98
+ source: {
99
+ type: "file",
100
+ text: {
101
+ value: attachment.content,
102
+ start: attachment.start,
103
+ end: attachment.end,
104
+ },
105
+ path,
106
+ },
107
+ } satisfies PromptRequestPart
108
+ })
109
+
110
+ const agents = input.prompt.filter(isAgentAttachment).map((attachment) => {
111
+ return {
112
+ id: Identifier.ascending("part"),
113
+ type: "agent",
114
+ name: attachment.name,
115
+ source: {
116
+ value: attachment.content,
117
+ start: attachment.start,
118
+ end: attachment.end,
119
+ },
120
+ } satisfies PromptRequestPart
121
+ })
122
+
123
+ const used = new Set(files.map((part) => part.url))
124
+ const context = input.context.flatMap((item) => {
125
+ const path = absolute(input.sessionDirectory, item.path)
126
+ const url = `file://${encodeFilePath(path)}${fileQuery(item.selection)}`
127
+ const comment = item.comment?.trim()
128
+ if (!comment && used.has(url)) return []
129
+ used.add(url)
130
+
131
+ const filePart = {
132
+ id: Identifier.ascending("part"),
133
+ type: "file",
134
+ mime: "text/plain",
135
+ url,
136
+ filename: getFilename(item.path),
137
+ } satisfies PromptRequestPart
138
+
139
+ if (!comment) return [filePart]
140
+
141
+ return [
142
+ {
143
+ id: Identifier.ascending("part"),
144
+ type: "text",
145
+ text: formatCommentNote({ path: item.path, selection: item.selection, comment }),
146
+ synthetic: true,
147
+ metadata: createCommentMetadata({
148
+ path: item.path,
149
+ selection: item.selection,
150
+ comment,
151
+ preview: item.preview,
152
+ origin: item.commentOrigin,
153
+ }),
154
+ } satisfies PromptRequestPart,
155
+ filePart,
156
+ ]
157
+ })
158
+
159
+ const images = input.images.map((attachment) => {
160
+ return {
161
+ id: Identifier.ascending("part"),
162
+ type: "file",
163
+ mime: attachment.mime,
164
+ url: attachment.dataUrl,
165
+ filename: attachment.filename,
166
+ } satisfies PromptRequestPart
167
+ })
168
+
169
+ requestParts.push(...files, ...context, ...agents, ...images)
170
+
171
+ return {
172
+ requestParts,
173
+ optimisticParts: requestParts.map((part) => toOptimisticPart(part, input.sessionID, input.messageID)),
174
+ }
175
+ }
@@ -0,0 +1,88 @@
1
+ import { Component, For, Show } from "solid-js"
2
+ import { FileIcon } from "@reign-labs/ui/file-icon"
3
+ import { IconButton } from "@reign-labs/ui/icon-button"
4
+ import { Tooltip } from "@reign-labs/ui/tooltip"
5
+ import { getDirectory, getFilename, getFilenameTruncated } from "@reign-labs/util/path"
6
+ import type { ContextItem } from "@/context/prompt"
7
+
8
+ type PromptContextItem = ContextItem & { key: string }
9
+
10
+ type ContextItemsProps = {
11
+ items: PromptContextItem[]
12
+ active: (item: PromptContextItem) => boolean
13
+ openComment: (item: PromptContextItem) => void
14
+ remove: (item: PromptContextItem) => void
15
+ t: (key: string) => string
16
+ }
17
+
18
+ export const PromptContextItems: Component<ContextItemsProps> = (props) => {
19
+ return (
20
+ <Show when={props.items.length > 0}>
21
+ <div class="flex flex-nowrap items-start gap-2 p-2 overflow-x-auto no-scrollbar">
22
+ <For each={props.items}>
23
+ {(item) => {
24
+ const directory = getDirectory(item.path)
25
+ const filename = getFilename(item.path)
26
+ const label = getFilenameTruncated(item.path, 14)
27
+ const selected = props.active(item)
28
+
29
+ return (
30
+ <Tooltip
31
+ value={
32
+ <span class="flex max-w-[300px]">
33
+ <span class="text-text-invert-base truncate-start [unicode-bidi:plaintext] min-w-0">
34
+ {directory}
35
+ </span>
36
+ <span class="shrink-0">{filename}</span>
37
+ </span>
38
+ }
39
+ placement="top"
40
+ openDelay={2000}
41
+ >
42
+ <div
43
+ classList={{
44
+ "group shrink-0 flex flex-col rounded-[6px] pl-2 pr-1 py-1 max-w-[200px] h-12 cursor-default transition-all transition-transform shadow-xs-border hover:shadow-xs-border-hover": true,
45
+ "hover:bg-surface-interactive-weak": !!item.commentID && !selected,
46
+ "bg-surface-interactive-hover hover:bg-surface-interactive-hover shadow-xs-border-hover": selected,
47
+ "bg-background-stronger": !selected,
48
+ }}
49
+ onClick={() => props.openComment(item)}
50
+ >
51
+ <div class="flex items-center gap-1.5">
52
+ <FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-3.5" />
53
+ <div class="flex items-center text-11-regular min-w-0 font-medium">
54
+ <span class="text-text-strong whitespace-nowrap">{label}</span>
55
+ <Show when={item.selection}>
56
+ {(sel) => (
57
+ <span class="text-text-weak whitespace-nowrap shrink-0">
58
+ {sel().startLine === sel().endLine
59
+ ? `:${sel().startLine}`
60
+ : `:${sel().startLine}-${sel().endLine}`}
61
+ </span>
62
+ )}
63
+ </Show>
64
+ </div>
65
+ <IconButton
66
+ type="button"
67
+ icon="close-small"
68
+ variant="ghost"
69
+ class="ml-auto size-3.5 text-text-weak hover:text-text-strong transition-all"
70
+ onClick={(e) => {
71
+ e.stopPropagation()
72
+ props.remove(item)
73
+ }}
74
+ aria-label={props.t("prompt.context.removeFile")}
75
+ />
76
+ </div>
77
+ <Show when={item.comment}>
78
+ {(comment) => <div class="text-12-regular text-text-strong ml-5 pr-1 truncate">{comment()}</div>}
79
+ </Show>
80
+ </div>
81
+ </Tooltip>
82
+ )
83
+ }}
84
+ </For>
85
+ </div>
86
+ </Show>
87
+ )
88
+ }
@@ -0,0 +1,25 @@
1
+ import { Component, Show } from "solid-js"
2
+ import { Icon } from "@reign-labs/ui/icon"
3
+
4
+ type PromptDragOverlayProps = {
5
+ type: "image" | "@mention" | null
6
+ label: string
7
+ }
8
+
9
+ const kindToIcon = {
10
+ image: "photo",
11
+ "@mention": "link",
12
+ } as const
13
+
14
+ export const PromptDragOverlay: Component<PromptDragOverlayProps> = (props) => {
15
+ return (
16
+ <Show when={props.type !== null}>
17
+ <div class="absolute inset-0 z-10 flex items-center justify-center bg-surface-raised-stronger-non-alpha/90 pointer-events-none">
18
+ <div class="flex flex-col items-center gap-2 text-text-weak">
19
+ <Icon name={props.type ? kindToIcon[props.type] : kindToIcon.image} class="size-8" />
20
+ <span class="text-14-regular">{props.label}</span>
21
+ </div>
22
+ </div>
23
+ </Show>
24
+ )
25
+ }
@@ -0,0 +1,99 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { createTextFragment, getCursorPosition, getNodeLength, getTextLength, setCursorPosition } from "./editor-dom"
3
+
4
+ describe("prompt-input editor dom", () => {
5
+ test("createTextFragment preserves newlines with consecutive br nodes", () => {
6
+ const fragment = createTextFragment("foo\n\nbar")
7
+ const container = document.createElement("div")
8
+ container.appendChild(fragment)
9
+
10
+ expect(container.childNodes.length).toBe(4)
11
+ expect(container.childNodes[0]?.textContent).toBe("foo")
12
+ expect((container.childNodes[1] as HTMLElement).tagName).toBe("BR")
13
+ expect((container.childNodes[2] as HTMLElement).tagName).toBe("BR")
14
+ expect(container.childNodes[3]?.textContent).toBe("bar")
15
+ })
16
+
17
+ test("createTextFragment keeps trailing newline as terminal break", () => {
18
+ const fragment = createTextFragment("foo\n")
19
+ const container = document.createElement("div")
20
+ container.appendChild(fragment)
21
+
22
+ expect(container.childNodes.length).toBe(2)
23
+ expect(container.childNodes[0]?.textContent).toBe("foo")
24
+ expect((container.childNodes[1] as HTMLElement).tagName).toBe("BR")
25
+ })
26
+
27
+ test("createTextFragment avoids break-node explosion for large multiline content", () => {
28
+ const content = Array.from({ length: 220 }, () => "line").join("\n")
29
+ const fragment = createTextFragment(content)
30
+ const container = document.createElement("div")
31
+ container.appendChild(fragment)
32
+
33
+ expect(container.childNodes.length).toBe(1)
34
+ expect(container.childNodes[0]?.nodeType).toBe(Node.TEXT_NODE)
35
+ expect(container.textContent).toBe(content)
36
+ })
37
+
38
+ test("createTextFragment keeps terminal break in large multiline fallback", () => {
39
+ const content = `${Array.from({ length: 220 }, () => "line").join("\n")}\n`
40
+ const fragment = createTextFragment(content)
41
+ const container = document.createElement("div")
42
+ container.appendChild(fragment)
43
+
44
+ expect(container.childNodes.length).toBe(2)
45
+ expect(container.childNodes[0]?.textContent).toBe(content.slice(0, -1))
46
+ expect((container.childNodes[1] as HTMLElement).tagName).toBe("BR")
47
+ })
48
+
49
+ test("length helpers treat breaks as one char and ignore zero-width chars", () => {
50
+ const container = document.createElement("div")
51
+ container.appendChild(document.createTextNode("ab\u200B"))
52
+ container.appendChild(document.createElement("br"))
53
+ container.appendChild(document.createTextNode("cd"))
54
+
55
+ expect(getNodeLength(container.childNodes[0]!)).toBe(2)
56
+ expect(getNodeLength(container.childNodes[1]!)).toBe(1)
57
+ expect(getTextLength(container)).toBe(5)
58
+ })
59
+
60
+ test("setCursorPosition and getCursorPosition round-trip with pills and breaks", () => {
61
+ const container = document.createElement("div")
62
+ const pill = document.createElement("span")
63
+ pill.dataset.type = "file"
64
+ pill.textContent = "@file"
65
+ container.appendChild(document.createTextNode("ab"))
66
+ container.appendChild(pill)
67
+ container.appendChild(document.createElement("br"))
68
+ container.appendChild(document.createTextNode("cd"))
69
+ document.body.appendChild(container)
70
+
71
+ setCursorPosition(container, 2)
72
+ expect(getCursorPosition(container)).toBe(2)
73
+
74
+ setCursorPosition(container, 7)
75
+ expect(getCursorPosition(container)).toBe(7)
76
+
77
+ setCursorPosition(container, 8)
78
+ expect(getCursorPosition(container)).toBe(8)
79
+
80
+ container.remove()
81
+ })
82
+
83
+ test("setCursorPosition and getCursorPosition round-trip across blank lines", () => {
84
+ const container = document.createElement("div")
85
+ container.appendChild(document.createTextNode("a"))
86
+ container.appendChild(document.createElement("br"))
87
+ container.appendChild(document.createElement("br"))
88
+ container.appendChild(document.createTextNode("b"))
89
+ document.body.appendChild(container)
90
+
91
+ setCursorPosition(container, 2)
92
+ expect(getCursorPosition(container)).toBe(2)
93
+
94
+ setCursorPosition(container, 3)
95
+ expect(getCursorPosition(container)).toBe(3)
96
+
97
+ container.remove()
98
+ })
99
+ })
@@ -0,0 +1,148 @@
1
+ const MAX_BREAKS = 200
2
+
3
+ export function createTextFragment(content: string): DocumentFragment {
4
+ const fragment = document.createDocumentFragment()
5
+ let breaks = 0
6
+ for (const char of content) {
7
+ if (char !== "\n") continue
8
+ breaks += 1
9
+ if (breaks > MAX_BREAKS) {
10
+ const tail = content.endsWith("\n")
11
+ const text = tail ? content.slice(0, -1) : content
12
+ if (text) fragment.appendChild(document.createTextNode(text))
13
+ if (tail) fragment.appendChild(document.createElement("br"))
14
+ return fragment
15
+ }
16
+ }
17
+
18
+ const segments = content.split("\n")
19
+ segments.forEach((segment, index) => {
20
+ if (segment) {
21
+ fragment.appendChild(document.createTextNode(segment))
22
+ }
23
+ if (index < segments.length - 1) {
24
+ fragment.appendChild(document.createElement("br"))
25
+ }
26
+ })
27
+ return fragment
28
+ }
29
+
30
+ export function getNodeLength(node: Node): number {
31
+ if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR") return 1
32
+ return (node.textContent ?? "").replace(/\u200B/g, "").length
33
+ }
34
+
35
+ export function getTextLength(node: Node): number {
36
+ if (node.nodeType === Node.TEXT_NODE) return (node.textContent ?? "").replace(/\u200B/g, "").length
37
+ if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR") return 1
38
+ let length = 0
39
+ for (const child of Array.from(node.childNodes)) {
40
+ length += getTextLength(child)
41
+ }
42
+ return length
43
+ }
44
+
45
+ export function getCursorPosition(parent: HTMLElement): number {
46
+ const selection = window.getSelection()
47
+ if (!selection || selection.rangeCount === 0) return 0
48
+ const range = selection.getRangeAt(0)
49
+ if (!parent.contains(range.startContainer)) return 0
50
+ const preCaretRange = range.cloneRange()
51
+ preCaretRange.selectNodeContents(parent)
52
+ preCaretRange.setEnd(range.startContainer, range.startOffset)
53
+ return getTextLength(preCaretRange.cloneContents())
54
+ }
55
+
56
+ export function setCursorPosition(parent: HTMLElement, position: number) {
57
+ let remaining = position
58
+ let node = parent.firstChild
59
+ while (node) {
60
+ const length = getNodeLength(node)
61
+ const isText = node.nodeType === Node.TEXT_NODE
62
+ const isPill =
63
+ node.nodeType === Node.ELEMENT_NODE &&
64
+ ((node as HTMLElement).dataset.type === "file" || (node as HTMLElement).dataset.type === "agent")
65
+ const isBreak = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR"
66
+
67
+ if (isText && remaining <= length) {
68
+ const range = document.createRange()
69
+ const selection = window.getSelection()
70
+ range.setStart(node, remaining)
71
+ range.collapse(true)
72
+ selection?.removeAllRanges()
73
+ selection?.addRange(range)
74
+ return
75
+ }
76
+
77
+ if ((isPill || isBreak) && remaining <= length) {
78
+ const range = document.createRange()
79
+ const selection = window.getSelection()
80
+ if (remaining === 0) {
81
+ range.setStartBefore(node)
82
+ }
83
+ if (remaining > 0 && isPill) {
84
+ range.setStartAfter(node)
85
+ }
86
+ if (remaining > 0 && isBreak) {
87
+ const next = node.nextSibling
88
+ if (next && next.nodeType === Node.TEXT_NODE) {
89
+ range.setStart(next, 0)
90
+ }
91
+ if (!next || next.nodeType !== Node.TEXT_NODE) {
92
+ range.setStartAfter(node)
93
+ }
94
+ }
95
+ range.collapse(true)
96
+ selection?.removeAllRanges()
97
+ selection?.addRange(range)
98
+ return
99
+ }
100
+
101
+ remaining -= length
102
+ node = node.nextSibling
103
+ }
104
+
105
+ const fallbackRange = document.createRange()
106
+ const fallbackSelection = window.getSelection()
107
+ const last = parent.lastChild
108
+ if (last && last.nodeType === Node.TEXT_NODE) {
109
+ const len = last.textContent ? last.textContent.length : 0
110
+ fallbackRange.setStart(last, len)
111
+ }
112
+ if (!last || last.nodeType !== Node.TEXT_NODE) {
113
+ fallbackRange.selectNodeContents(parent)
114
+ }
115
+ fallbackRange.collapse(false)
116
+ fallbackSelection?.removeAllRanges()
117
+ fallbackSelection?.addRange(fallbackRange)
118
+ }
119
+
120
+ export function setRangeEdge(parent: HTMLElement, range: Range, edge: "start" | "end", offset: number) {
121
+ let remaining = offset
122
+ const nodes = Array.from(parent.childNodes)
123
+
124
+ for (const node of nodes) {
125
+ const length = getNodeLength(node)
126
+ const isText = node.nodeType === Node.TEXT_NODE
127
+ const isPill =
128
+ node.nodeType === Node.ELEMENT_NODE &&
129
+ ((node as HTMLElement).dataset.type === "file" || (node as HTMLElement).dataset.type === "agent")
130
+ const isBreak = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR"
131
+
132
+ if (isText && remaining <= length) {
133
+ if (edge === "start") range.setStart(node, remaining)
134
+ if (edge === "end") range.setEnd(node, remaining)
135
+ return
136
+ }
137
+
138
+ if ((isPill || isBreak) && remaining <= length) {
139
+ if (edge === "start" && remaining === 0) range.setStartBefore(node)
140
+ if (edge === "start" && remaining > 0) range.setStartAfter(node)
141
+ if (edge === "end" && remaining === 0) range.setEndBefore(node)
142
+ if (edge === "end" && remaining > 0) range.setEndAfter(node)
143
+ return
144
+ }
145
+
146
+ remaining -= length
147
+ }
148
+ }
@@ -0,0 +1,66 @@
1
+ import { ACCEPTED_FILE_TYPES, ACCEPTED_IMAGE_TYPES } from "@/constants/file-picker"
2
+
3
+ export { ACCEPTED_FILE_TYPES }
4
+
5
+ const IMAGE_MIMES = new Set(ACCEPTED_IMAGE_TYPES)
6
+ const IMAGE_EXTS = new Map([
7
+ ["gif", "image/gif"],
8
+ ["jpeg", "image/jpeg"],
9
+ ["jpg", "image/jpeg"],
10
+ ["png", "image/png"],
11
+ ["webp", "image/webp"],
12
+ ])
13
+ const TEXT_MIMES = new Set([
14
+ "application/json",
15
+ "application/ld+json",
16
+ "application/toml",
17
+ "application/x-toml",
18
+ "application/x-yaml",
19
+ "application/xml",
20
+ "application/yaml",
21
+ ])
22
+
23
+ const SAMPLE = 4096
24
+
25
+ function kind(type: string) {
26
+ return type.split(";", 1)[0]?.trim().toLowerCase() ?? ""
27
+ }
28
+
29
+ function ext(name: string) {
30
+ const idx = name.lastIndexOf(".")
31
+ if (idx === -1) return ""
32
+ return name.slice(idx + 1).toLowerCase()
33
+ }
34
+
35
+ function textMime(type: string) {
36
+ if (!type) return false
37
+ if (type.startsWith("text/")) return true
38
+ if (TEXT_MIMES.has(type)) return true
39
+ if (type.endsWith("+json")) return true
40
+ return type.endsWith("+xml")
41
+ }
42
+
43
+ function textBytes(bytes: Uint8Array) {
44
+ if (bytes.length === 0) return true
45
+ let count = 0
46
+ for (const byte of bytes) {
47
+ if (byte === 0) return false
48
+ if (byte < 9 || (byte > 13 && byte < 32)) count += 1
49
+ }
50
+ return count / bytes.length <= 0.3
51
+ }
52
+
53
+ export async function attachmentMime(file: File) {
54
+ const type = kind(file.type)
55
+ if (IMAGE_MIMES.has(type)) return type
56
+ if (type === "application/pdf") return type
57
+
58
+ const suffix = ext(file.name)
59
+ const fallback = IMAGE_EXTS.get(suffix) ?? (suffix === "pdf" ? "application/pdf" : undefined)
60
+ if ((!type || type === "application/octet-stream") && fallback) return fallback
61
+
62
+ if (textMime(type)) return "text/plain"
63
+ const bytes = new Uint8Array(await file.slice(0, SAMPLE).arrayBuffer())
64
+ if (!textBytes(bytes)) return
65
+ return "text/plain"
66
+ }