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,153 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import type { Prompt } from "@/context/prompt"
3
+ import {
4
+ canNavigateHistoryAtCursor,
5
+ clonePromptParts,
6
+ normalizePromptHistoryEntry,
7
+ navigatePromptHistory,
8
+ prependHistoryEntry,
9
+ promptLength,
10
+ type PromptHistoryComment,
11
+ } from "./history"
12
+
13
+ const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
14
+
15
+ const text = (value: string): Prompt => [{ type: "text", content: value, start: 0, end: value.length }]
16
+ const comment = (id: string, value = "note"): PromptHistoryComment => ({
17
+ id,
18
+ path: "src/a.ts",
19
+ selection: { start: 2, end: 4 },
20
+ comment: value,
21
+ time: 1,
22
+ origin: "review",
23
+ preview: "const a = 1",
24
+ })
25
+
26
+ describe("prompt-input history", () => {
27
+ test("prependHistoryEntry skips empty prompt and deduplicates consecutive entries", () => {
28
+ const first = prependHistoryEntry([], DEFAULT_PROMPT)
29
+ expect(first).toEqual([])
30
+
31
+ const commentsOnly = prependHistoryEntry([], DEFAULT_PROMPT, [comment("c1")])
32
+ expect(commentsOnly).toHaveLength(1)
33
+
34
+ const withOne = prependHistoryEntry([], text("hello"))
35
+ expect(withOne).toHaveLength(1)
36
+
37
+ const deduped = prependHistoryEntry(withOne, text("hello"))
38
+ expect(deduped).toBe(withOne)
39
+
40
+ const dedupedComments = prependHistoryEntry(commentsOnly, DEFAULT_PROMPT, [comment("c1")])
41
+ expect(dedupedComments).toBe(commentsOnly)
42
+ })
43
+
44
+ test("navigatePromptHistory restores saved prompt when moving down from newest", () => {
45
+ const entries = [text("third"), text("second"), text("first")]
46
+ const up = navigatePromptHistory({
47
+ direction: "up",
48
+ entries,
49
+ historyIndex: -1,
50
+ currentPrompt: text("draft"),
51
+ currentComments: [comment("draft")],
52
+ savedPrompt: null,
53
+ })
54
+ expect(up.handled).toBe(true)
55
+ if (!up.handled) throw new Error("expected handled")
56
+ expect(up.historyIndex).toBe(0)
57
+ expect(up.cursor).toBe("start")
58
+ expect(up.entry.comments).toEqual([])
59
+
60
+ const down = navigatePromptHistory({
61
+ direction: "down",
62
+ entries,
63
+ historyIndex: up.historyIndex,
64
+ currentPrompt: text("ignored"),
65
+ currentComments: [],
66
+ savedPrompt: up.savedPrompt,
67
+ })
68
+ expect(down.handled).toBe(true)
69
+ if (!down.handled) throw new Error("expected handled")
70
+ expect(down.historyIndex).toBe(-1)
71
+ expect(down.entry.prompt[0]?.type === "text" ? down.entry.prompt[0].content : "").toBe("draft")
72
+ expect(down.entry.comments).toEqual([comment("draft")])
73
+ })
74
+
75
+ test("navigatePromptHistory keeps entry comments when moving through history", () => {
76
+ const entries = [
77
+ {
78
+ prompt: text("with comment"),
79
+ comments: [comment("c1")],
80
+ },
81
+ ]
82
+
83
+ const up = navigatePromptHistory({
84
+ direction: "up",
85
+ entries,
86
+ historyIndex: -1,
87
+ currentPrompt: text("draft"),
88
+ currentComments: [],
89
+ savedPrompt: null,
90
+ })
91
+
92
+ expect(up.handled).toBe(true)
93
+ if (!up.handled) throw new Error("expected handled")
94
+ expect(up.entry.prompt[0]?.type === "text" ? up.entry.prompt[0].content : "").toBe("with comment")
95
+ expect(up.entry.comments).toEqual([comment("c1")])
96
+ })
97
+
98
+ test("normalizePromptHistoryEntry supports legacy prompt arrays", () => {
99
+ const entry = normalizePromptHistoryEntry(text("legacy"))
100
+ expect(entry.prompt[0]?.type === "text" ? entry.prompt[0].content : "").toBe("legacy")
101
+ expect(entry.comments).toEqual([])
102
+ })
103
+
104
+ test("helpers clone prompt and count text content length", () => {
105
+ const original: Prompt = [
106
+ { type: "text", content: "one", start: 0, end: 3 },
107
+ {
108
+ type: "file",
109
+ path: "src/a.ts",
110
+ content: "@src/a.ts",
111
+ start: 3,
112
+ end: 12,
113
+ selection: { startLine: 1, startChar: 1, endLine: 2, endChar: 1 },
114
+ },
115
+ { type: "image", id: "1", filename: "img.png", mime: "image/png", dataUrl: "data:image/png;base64,abc" },
116
+ ]
117
+ const copy = clonePromptParts(original)
118
+ expect(copy).not.toBe(original)
119
+ expect(promptLength(copy)).toBe(12)
120
+ if (copy[1]?.type !== "file") throw new Error("expected file")
121
+ copy[1].selection!.startLine = 9
122
+ if (original[1]?.type !== "file") throw new Error("expected file")
123
+ expect(original[1].selection?.startLine).toBe(1)
124
+ })
125
+
126
+ test("canNavigateHistoryAtCursor only allows prompt boundaries", () => {
127
+ const value = "a\nb\nc"
128
+
129
+ expect(canNavigateHistoryAtCursor("up", value, 0)).toBe(false)
130
+ expect(canNavigateHistoryAtCursor("down", value, 0)).toBe(false)
131
+
132
+ expect(canNavigateHistoryAtCursor("up", value, 2)).toBe(false)
133
+ expect(canNavigateHistoryAtCursor("down", value, 2)).toBe(false)
134
+
135
+ expect(canNavigateHistoryAtCursor("up", value, 5)).toBe(false)
136
+ expect(canNavigateHistoryAtCursor("down", value, 5)).toBe(true)
137
+
138
+ expect(canNavigateHistoryAtCursor("up", "abc", 0)).toBe(false)
139
+ expect(canNavigateHistoryAtCursor("down", "abc", 3)).toBe(true)
140
+ expect(canNavigateHistoryAtCursor("up", "abc", 1)).toBe(false)
141
+ expect(canNavigateHistoryAtCursor("down", "abc", 1)).toBe(false)
142
+
143
+ expect(canNavigateHistoryAtCursor("up", "", 0)).toBe(true)
144
+ expect(canNavigateHistoryAtCursor("down", "", 0)).toBe(true)
145
+
146
+ expect(canNavigateHistoryAtCursor("up", "abc", 0, true)).toBe(true)
147
+ expect(canNavigateHistoryAtCursor("up", "abc", 3, true)).toBe(true)
148
+ expect(canNavigateHistoryAtCursor("down", "abc", 0, true)).toBe(true)
149
+ expect(canNavigateHistoryAtCursor("down", "abc", 3, true)).toBe(true)
150
+ expect(canNavigateHistoryAtCursor("up", "abc", 1, true)).toBe(false)
151
+ expect(canNavigateHistoryAtCursor("down", "abc", 1, true)).toBe(false)
152
+ })
153
+ })
@@ -0,0 +1,256 @@
1
+ import type { Prompt } from "@/context/prompt"
2
+ import type { SelectedLineRange } from "@/context/file"
3
+
4
+ const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
5
+
6
+ export const MAX_HISTORY = 100
7
+
8
+ export type PromptHistoryComment = {
9
+ id: string
10
+ path: string
11
+ selection: SelectedLineRange
12
+ comment: string
13
+ time: number
14
+ origin?: "review" | "file"
15
+ preview?: string
16
+ }
17
+
18
+ export type PromptHistoryEntry = {
19
+ prompt: Prompt
20
+ comments: PromptHistoryComment[]
21
+ }
22
+
23
+ export type PromptHistoryStoredEntry = Prompt | PromptHistoryEntry
24
+
25
+ export function canNavigateHistoryAtCursor(direction: "up" | "down", text: string, cursor: number, inHistory = false) {
26
+ const position = Math.max(0, Math.min(cursor, text.length))
27
+ const atStart = position === 0
28
+ const atEnd = position === text.length
29
+ if (inHistory) return atStart || atEnd
30
+ if (direction === "up") return position === 0 && text.length === 0
31
+ return position === text.length
32
+ }
33
+
34
+ export function clonePromptParts(prompt: Prompt): Prompt {
35
+ return prompt.map((part) => {
36
+ if (part.type === "text") return { ...part }
37
+ if (part.type === "image") return { ...part }
38
+ if (part.type === "agent") return { ...part }
39
+ return {
40
+ ...part,
41
+ selection: part.selection ? { ...part.selection } : undefined,
42
+ }
43
+ })
44
+ }
45
+
46
+ function cloneSelection(selection: SelectedLineRange): SelectedLineRange {
47
+ return {
48
+ start: selection.start,
49
+ end: selection.end,
50
+ ...(selection.side ? { side: selection.side } : {}),
51
+ ...(selection.endSide ? { endSide: selection.endSide } : {}),
52
+ }
53
+ }
54
+
55
+ export function clonePromptHistoryComments(comments: PromptHistoryComment[]) {
56
+ return comments.map((comment) => ({
57
+ ...comment,
58
+ selection: cloneSelection(comment.selection),
59
+ }))
60
+ }
61
+
62
+ export function normalizePromptHistoryEntry(entry: PromptHistoryStoredEntry): PromptHistoryEntry {
63
+ if (Array.isArray(entry)) {
64
+ return {
65
+ prompt: clonePromptParts(entry),
66
+ comments: [],
67
+ }
68
+ }
69
+ return {
70
+ prompt: clonePromptParts(entry.prompt),
71
+ comments: clonePromptHistoryComments(entry.comments),
72
+ }
73
+ }
74
+
75
+ export function promptLength(prompt: Prompt) {
76
+ return prompt.reduce((len, part) => len + ("content" in part ? part.content.length : 0), 0)
77
+ }
78
+
79
+ export function prependHistoryEntry(
80
+ entries: PromptHistoryStoredEntry[],
81
+ prompt: Prompt,
82
+ comments: PromptHistoryComment[] = [],
83
+ max = MAX_HISTORY,
84
+ ) {
85
+ const text = prompt
86
+ .map((part) => ("content" in part ? part.content : ""))
87
+ .join("")
88
+ .trim()
89
+ const hasImages = prompt.some((part) => part.type === "image")
90
+ const hasComments = comments.some((comment) => !!comment.comment.trim())
91
+ if (!text && !hasImages && !hasComments) return entries
92
+
93
+ const entry = {
94
+ prompt: clonePromptParts(prompt),
95
+ comments: clonePromptHistoryComments(comments),
96
+ } satisfies PromptHistoryEntry
97
+ const last = entries[0]
98
+ if (last && isPromptEqual(last, entry)) return entries
99
+ return [entry, ...entries].slice(0, max)
100
+ }
101
+
102
+ function isCommentEqual(commentA: PromptHistoryComment, commentB: PromptHistoryComment) {
103
+ return (
104
+ commentA.path === commentB.path &&
105
+ commentA.comment === commentB.comment &&
106
+ commentA.origin === commentB.origin &&
107
+ commentA.preview === commentB.preview &&
108
+ commentA.selection.start === commentB.selection.start &&
109
+ commentA.selection.end === commentB.selection.end &&
110
+ commentA.selection.side === commentB.selection.side &&
111
+ commentA.selection.endSide === commentB.selection.endSide
112
+ )
113
+ }
114
+
115
+ function isPromptEqual(promptA: PromptHistoryStoredEntry, promptB: PromptHistoryStoredEntry) {
116
+ const entryA = normalizePromptHistoryEntry(promptA)
117
+ const entryB = normalizePromptHistoryEntry(promptB)
118
+ if (entryA.prompt.length !== entryB.prompt.length) return false
119
+ for (let i = 0; i < entryA.prompt.length; i++) {
120
+ const partA = entryA.prompt[i]
121
+ const partB = entryB.prompt[i]
122
+ if (partA.type !== partB.type) return false
123
+ if (partA.type === "text" && partA.content !== (partB.type === "text" ? partB.content : "")) return false
124
+ if (partA.type === "file") {
125
+ if (partA.path !== (partB.type === "file" ? partB.path : "")) return false
126
+ const a = partA.selection
127
+ const b = partB.type === "file" ? partB.selection : undefined
128
+ const sameSelection =
129
+ (!a && !b) ||
130
+ (!!a &&
131
+ !!b &&
132
+ a.startLine === b.startLine &&
133
+ a.startChar === b.startChar &&
134
+ a.endLine === b.endLine &&
135
+ a.endChar === b.endChar)
136
+ if (!sameSelection) return false
137
+ }
138
+ if (partA.type === "agent" && partA.name !== (partB.type === "agent" ? partB.name : "")) return false
139
+ if (partA.type === "image" && partA.id !== (partB.type === "image" ? partB.id : "")) return false
140
+ }
141
+ if (entryA.comments.length !== entryB.comments.length) return false
142
+ for (let i = 0; i < entryA.comments.length; i++) {
143
+ const commentA = entryA.comments[i]
144
+ const commentB = entryB.comments[i]
145
+ if (!commentA || !commentB || !isCommentEqual(commentA, commentB)) return false
146
+ }
147
+ return true
148
+ }
149
+
150
+ type HistoryNavInput = {
151
+ direction: "up" | "down"
152
+ entries: PromptHistoryStoredEntry[]
153
+ historyIndex: number
154
+ currentPrompt: Prompt
155
+ currentComments: PromptHistoryComment[]
156
+ savedPrompt: PromptHistoryEntry | null
157
+ }
158
+
159
+ type HistoryNavResult =
160
+ | {
161
+ handled: false
162
+ historyIndex: number
163
+ savedPrompt: PromptHistoryEntry | null
164
+ }
165
+ | {
166
+ handled: true
167
+ historyIndex: number
168
+ savedPrompt: PromptHistoryEntry | null
169
+ entry: PromptHistoryEntry
170
+ cursor: "start" | "end"
171
+ }
172
+
173
+ export function navigatePromptHistory(input: HistoryNavInput): HistoryNavResult {
174
+ if (input.direction === "up") {
175
+ if (input.entries.length === 0) {
176
+ return {
177
+ handled: false,
178
+ historyIndex: input.historyIndex,
179
+ savedPrompt: input.savedPrompt,
180
+ }
181
+ }
182
+
183
+ if (input.historyIndex === -1) {
184
+ const entry = normalizePromptHistoryEntry(input.entries[0])
185
+ return {
186
+ handled: true,
187
+ historyIndex: 0,
188
+ savedPrompt: {
189
+ prompt: clonePromptParts(input.currentPrompt),
190
+ comments: clonePromptHistoryComments(input.currentComments),
191
+ },
192
+ entry,
193
+ cursor: "start",
194
+ }
195
+ }
196
+
197
+ if (input.historyIndex < input.entries.length - 1) {
198
+ const next = input.historyIndex + 1
199
+ const entry = normalizePromptHistoryEntry(input.entries[next])
200
+ return {
201
+ handled: true,
202
+ historyIndex: next,
203
+ savedPrompt: input.savedPrompt,
204
+ entry,
205
+ cursor: "start",
206
+ }
207
+ }
208
+
209
+ return {
210
+ handled: false,
211
+ historyIndex: input.historyIndex,
212
+ savedPrompt: input.savedPrompt,
213
+ }
214
+ }
215
+
216
+ if (input.historyIndex > 0) {
217
+ const next = input.historyIndex - 1
218
+ const entry = normalizePromptHistoryEntry(input.entries[next])
219
+ return {
220
+ handled: true,
221
+ historyIndex: next,
222
+ savedPrompt: input.savedPrompt,
223
+ entry,
224
+ cursor: "end",
225
+ }
226
+ }
227
+
228
+ if (input.historyIndex === 0) {
229
+ if (input.savedPrompt) {
230
+ return {
231
+ handled: true,
232
+ historyIndex: -1,
233
+ savedPrompt: null,
234
+ entry: input.savedPrompt,
235
+ cursor: "end",
236
+ }
237
+ }
238
+
239
+ return {
240
+ handled: true,
241
+ historyIndex: -1,
242
+ savedPrompt: null,
243
+ entry: {
244
+ prompt: DEFAULT_PROMPT,
245
+ comments: [],
246
+ },
247
+ cursor: "end",
248
+ }
249
+ }
250
+
251
+ return {
252
+ handled: false,
253
+ historyIndex: input.historyIndex,
254
+ savedPrompt: input.savedPrompt,
255
+ }
256
+ }
@@ -0,0 +1,58 @@
1
+ import { Component, For, Show } from "solid-js"
2
+ import { Icon } from "@reign-labs/ui/icon"
3
+ import type { ImageAttachmentPart } from "@/context/prompt"
4
+
5
+ type PromptImageAttachmentsProps = {
6
+ attachments: ImageAttachmentPart[]
7
+ onOpen: (attachment: ImageAttachmentPart) => void
8
+ onRemove: (id: string) => void
9
+ removeLabel: string
10
+ }
11
+
12
+ const fallbackClass = "size-16 rounded-md bg-surface-base flex items-center justify-center border border-border-base"
13
+ const imageClass =
14
+ "size-16 rounded-md object-cover border border-border-base hover:border-border-strong-base transition-colors"
15
+ const removeClass =
16
+ "absolute -top-1.5 -right-1.5 size-5 rounded-full bg-surface-raised-stronger-non-alpha border border-border-base flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity hover:bg-surface-raised-base-hover"
17
+ const nameClass = "absolute bottom-0 left-0 right-0 px-1 py-0.5 bg-black/50 rounded-b-md"
18
+
19
+ export const PromptImageAttachments: Component<PromptImageAttachmentsProps> = (props) => {
20
+ return (
21
+ <Show when={props.attachments.length > 0}>
22
+ <div class="flex flex-wrap gap-2 px-3 pt-3">
23
+ <For each={props.attachments}>
24
+ {(attachment) => (
25
+ <div class="relative group">
26
+ <Show
27
+ when={attachment.mime.startsWith("image/")}
28
+ fallback={
29
+ <div class={fallbackClass}>
30
+ <Icon name="folder" class="size-6 text-text-weak" />
31
+ </div>
32
+ }
33
+ >
34
+ <img
35
+ src={attachment.dataUrl}
36
+ alt={attachment.filename}
37
+ class={imageClass}
38
+ onClick={() => props.onOpen(attachment)}
39
+ />
40
+ </Show>
41
+ <button
42
+ type="button"
43
+ onClick={() => props.onRemove(attachment.id)}
44
+ class={removeClass}
45
+ aria-label={props.removeLabel}
46
+ >
47
+ <Icon name="close" class="size-3 text-text-weak" />
48
+ </button>
49
+ <div class={nameClass}>
50
+ <span class="text-10-regular text-white truncate block">{attachment.filename}</span>
51
+ </div>
52
+ </div>
53
+ )}
54
+ </For>
55
+ </div>
56
+ </Show>
57
+ )
58
+ }
@@ -0,0 +1,24 @@
1
+ const LARGE_PASTE_CHARS = 8000
2
+ const LARGE_PASTE_BREAKS = 120
3
+
4
+ function largePaste(text: string) {
5
+ if (text.length >= LARGE_PASTE_CHARS) return true
6
+ let breaks = 0
7
+ for (const char of text) {
8
+ if (char !== "\n") continue
9
+ breaks += 1
10
+ if (breaks >= LARGE_PASTE_BREAKS) return true
11
+ }
12
+ return false
13
+ }
14
+
15
+ export function normalizePaste(text: string) {
16
+ if (!text.includes("\r")) return text
17
+ return text.replace(/\r\n?/g, "\n")
18
+ }
19
+
20
+ export function pasteMode(text: string) {
21
+ if (largePaste(text)) return "manual"
22
+ if (text.includes("\n") || text.includes("\r")) return "manual"
23
+ return "native"
24
+ }
@@ -0,0 +1,48 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { promptPlaceholder } from "./placeholder"
3
+
4
+ describe("promptPlaceholder", () => {
5
+ const t = (key: string, params?: Record<string, string>) => `${key}${params?.example ? `:${params.example}` : ""}`
6
+
7
+ test("returns shell placeholder in shell mode", () => {
8
+ const value = promptPlaceholder({
9
+ mode: "shell",
10
+ commentCount: 0,
11
+ example: "example",
12
+ suggest: true,
13
+ t,
14
+ })
15
+ expect(value).toBe("prompt.placeholder.shell")
16
+ })
17
+
18
+ test("returns summarize placeholders for comment context", () => {
19
+ expect(promptPlaceholder({ mode: "normal", commentCount: 1, example: "example", suggest: true, t })).toBe(
20
+ "prompt.placeholder.summarizeComment",
21
+ )
22
+ expect(promptPlaceholder({ mode: "normal", commentCount: 2, example: "example", suggest: true, t })).toBe(
23
+ "prompt.placeholder.summarizeComments",
24
+ )
25
+ })
26
+
27
+ test("returns default placeholder with example when suggestions enabled", () => {
28
+ const value = promptPlaceholder({
29
+ mode: "normal",
30
+ commentCount: 0,
31
+ example: "translated-example",
32
+ suggest: true,
33
+ t,
34
+ })
35
+ expect(value).toBe("prompt.placeholder.normal:translated-example")
36
+ })
37
+
38
+ test("returns simple placeholder when suggestions disabled", () => {
39
+ const value = promptPlaceholder({
40
+ mode: "normal",
41
+ commentCount: 0,
42
+ example: "translated-example",
43
+ suggest: false,
44
+ t,
45
+ })
46
+ expect(value).toBe("prompt.placeholder.simple")
47
+ })
48
+ })
@@ -0,0 +1,15 @@
1
+ type PromptPlaceholderInput = {
2
+ mode: "normal" | "shell"
3
+ commentCount: number
4
+ example: string
5
+ suggest: boolean
6
+ t: (key: string, params?: Record<string, string>) => string
7
+ }
8
+
9
+ export function promptPlaceholder(input: PromptPlaceholderInput) {
10
+ if (input.mode === "shell") return input.t("prompt.placeholder.shell")
11
+ if (input.commentCount > 1) return input.t("prompt.placeholder.summarizeComments")
12
+ if (input.commentCount === 1) return input.t("prompt.placeholder.summarizeComment")
13
+ if (!input.suggest) return input.t("prompt.placeholder.simple")
14
+ return input.t("prompt.placeholder.normal", { example: input.example })
15
+ }