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,426 @@
1
+ import { waitSessionIdle, withSession } from "../actions"
2
+ import { test, expect } from "../fixtures"
3
+ import { createSdk } from "../utils"
4
+
5
+ const count = 14
6
+
7
+ function body(mark: string) {
8
+ return [
9
+ `title ${mark}`,
10
+ `mark ${mark}`,
11
+ ...Array.from({ length: 32 }, (_, i) => `line ${String(i + 1).padStart(2, "0")} ${mark}`),
12
+ ]
13
+ }
14
+
15
+ function files(tag: string) {
16
+ return Array.from({ length: count }, (_, i) => {
17
+ const id = String(i).padStart(2, "0")
18
+ return {
19
+ file: `review-scroll-${id}.txt`,
20
+ mark: `${tag}-${id}`,
21
+ }
22
+ })
23
+ }
24
+
25
+ function seed(list: ReturnType<typeof files>) {
26
+ const out = ["*** Begin Patch"]
27
+
28
+ for (const item of list) {
29
+ out.push(`*** Add File: ${item.file}`)
30
+ for (const line of body(item.mark)) out.push(`+${line}`)
31
+ }
32
+
33
+ out.push("*** End Patch")
34
+ return out.join("\n")
35
+ }
36
+
37
+ function edit(file: string, prev: string, next: string) {
38
+ return ["*** Begin Patch", `*** Update File: ${file}`, "@@", `-mark ${prev}`, `+mark ${next}`, "*** End Patch"].join(
39
+ "\n",
40
+ )
41
+ }
42
+
43
+ async function patch(sdk: ReturnType<typeof createSdk>, sessionID: string, patchText: string) {
44
+ await sdk.session.promptAsync({
45
+ sessionID,
46
+ agent: "build",
47
+ system: [
48
+ "You are seeding deterministic e2e UI state.",
49
+ "Your only valid response is one apply_patch tool call.",
50
+ `Use this JSON input: ${JSON.stringify({ patchText })}`,
51
+ "Do not call any other tools.",
52
+ "Do not output plain text.",
53
+ ].join("\n"),
54
+ parts: [{ type: "text", text: "Apply the provided patch exactly once." }],
55
+ })
56
+
57
+ await waitSessionIdle(sdk, sessionID, 120_000)
58
+ }
59
+
60
+ async function show(page: Parameters<typeof test>[0]["page"]) {
61
+ const btn = page.getByRole("button", { name: "Toggle review" }).first()
62
+ await expect(btn).toBeVisible()
63
+ if ((await btn.getAttribute("aria-expanded")) !== "true") await btn.click()
64
+ await expect(btn).toHaveAttribute("aria-expanded", "true")
65
+ }
66
+
67
+ async function expand(page: Parameters<typeof test>[0]["page"]) {
68
+ const close = page.getByRole("button", { name: /^Collapse all$/i }).first()
69
+ const open = await close
70
+ .isVisible()
71
+ .then((value) => value)
72
+ .catch(() => false)
73
+
74
+ const btn = page.getByRole("button", { name: /^Expand all$/i }).first()
75
+ if (open) {
76
+ await close.click()
77
+ await expect(btn).toBeVisible()
78
+ }
79
+
80
+ await expect(btn).toBeVisible()
81
+ await btn.click()
82
+ await expect(close).toBeVisible()
83
+ }
84
+
85
+ async function waitMark(page: Parameters<typeof test>[0]["page"], file: string, mark: string) {
86
+ await page.waitForFunction(
87
+ ({ file, mark }) => {
88
+ const view = document.querySelector('[data-slot="session-review-scroll"] .scroll-view__viewport')
89
+ if (!(view instanceof HTMLElement)) return false
90
+
91
+ const head = Array.from(view.querySelectorAll("h3")).find(
92
+ (node) => node instanceof HTMLElement && node.textContent?.includes(file),
93
+ )
94
+ if (!(head instanceof HTMLElement)) return false
95
+
96
+ return Array.from(head.parentElement?.querySelectorAll("diffs-container") ?? []).some((host) => {
97
+ if (!(host instanceof HTMLElement)) return false
98
+ const root = host.shadowRoot
99
+ return root?.textContent?.includes(`mark ${mark}`) ?? false
100
+ })
101
+ },
102
+ { file, mark },
103
+ { timeout: 60_000 },
104
+ )
105
+ }
106
+
107
+ async function spot(page: Parameters<typeof test>[0]["page"], file: string) {
108
+ return page.evaluate((file) => {
109
+ const view = document.querySelector('[data-slot="session-review-scroll"] .scroll-view__viewport')
110
+ if (!(view instanceof HTMLElement)) return null
111
+
112
+ const row = Array.from(view.querySelectorAll("h3")).find(
113
+ (node) => node instanceof HTMLElement && node.textContent?.includes(file),
114
+ )
115
+ if (!(row instanceof HTMLElement)) return null
116
+
117
+ const a = row.getBoundingClientRect()
118
+ const b = view.getBoundingClientRect()
119
+ return {
120
+ top: a.top - b.top,
121
+ y: view.scrollTop,
122
+ }
123
+ }, file)
124
+ }
125
+
126
+ async function comment(page: Parameters<typeof test>[0]["page"], file: string, note: string) {
127
+ const row = page.locator(`[data-file="${file}"]`).first()
128
+ await expect(row).toBeVisible()
129
+
130
+ const line = row.locator('diffs-container [data-line="2"]').first()
131
+ await expect(line).toBeVisible()
132
+ await line.hover()
133
+
134
+ const add = row.getByRole("button", { name: /^Comment$/ }).first()
135
+ await expect(add).toBeVisible()
136
+ await add.click()
137
+
138
+ const area = row.locator('[data-slot="line-comment-textarea"]').first()
139
+ await expect(area).toBeVisible()
140
+ await area.fill(note)
141
+
142
+ const submit = row.locator('[data-slot="line-comment-action"][data-variant="primary"]').first()
143
+ await expect(submit).toBeEnabled()
144
+ await submit.click()
145
+
146
+ await expect(row.locator('[data-slot="line-comment-content"]').filter({ hasText: note }).first()).toBeVisible()
147
+ await expect(row.locator('[data-slot="line-comment-tools"]').first()).toBeVisible()
148
+ }
149
+
150
+ async function overflow(page: Parameters<typeof test>[0]["page"], file: string) {
151
+ const row = page.locator(`[data-file="${file}"]`).first()
152
+ const view = page.locator('[data-slot="session-review-scroll"] .scroll-view__viewport').first()
153
+ const pop = row.locator('[data-slot="line-comment-popover"][data-inline-body]').first()
154
+ const tools = row.locator('[data-slot="line-comment-tools"]').first()
155
+
156
+ const [width, viewBox, popBox, toolsBox] = await Promise.all([
157
+ view.evaluate((el) => el.scrollWidth - el.clientWidth),
158
+ view.boundingBox(),
159
+ pop.boundingBox(),
160
+ tools.boundingBox(),
161
+ ])
162
+
163
+ if (!viewBox || !popBox || !toolsBox) return null
164
+
165
+ return {
166
+ width,
167
+ pop: popBox.x + popBox.width - (viewBox.x + viewBox.width),
168
+ tools: toolsBox.x + toolsBox.width - (viewBox.x + viewBox.width),
169
+ }
170
+ }
171
+
172
+ async function openReviewFile(page: Parameters<typeof test>[0]["page"], file: string) {
173
+ const row = page.locator(`[data-file="${file}"]`).first()
174
+ await expect(row).toBeVisible()
175
+ await row.hover()
176
+
177
+ const open = row.getByRole("button", { name: /^Open file$/i }).first()
178
+ await expect(open).toBeVisible()
179
+ await open.click()
180
+
181
+ const tab = page.getByRole("tab", { name: file }).first()
182
+ await expect(tab).toBeVisible()
183
+ await tab.click()
184
+
185
+ const viewer = page.locator('[data-component="file"][data-mode="text"]').first()
186
+ await expect(viewer).toBeVisible()
187
+ return viewer
188
+ }
189
+
190
+ async function fileComment(page: Parameters<typeof test>[0]["page"], note: string) {
191
+ const viewer = page.locator('[data-component="file"][data-mode="text"]').first()
192
+ await expect(viewer).toBeVisible()
193
+
194
+ const line = viewer.locator('diffs-container [data-line="2"]').first()
195
+ await expect(line).toBeVisible()
196
+ await line.hover()
197
+
198
+ const add = viewer.getByRole("button", { name: /^Comment$/ }).first()
199
+ await expect(add).toBeVisible()
200
+ await add.click()
201
+
202
+ const area = viewer.locator('[data-slot="line-comment-textarea"]').first()
203
+ await expect(area).toBeVisible()
204
+ await area.fill(note)
205
+
206
+ const submit = viewer.locator('[data-slot="line-comment-action"][data-variant="primary"]').first()
207
+ await expect(submit).toBeEnabled()
208
+ await submit.click()
209
+
210
+ await expect(viewer.locator('[data-slot="line-comment-content"]').filter({ hasText: note }).first()).toBeVisible()
211
+ await expect(viewer.locator('[data-slot="line-comment-tools"]').first()).toBeVisible()
212
+ }
213
+
214
+ async function fileOverflow(page: Parameters<typeof test>[0]["page"]) {
215
+ const viewer = page.locator('[data-component="file"][data-mode="text"]').first()
216
+ const view = page.locator('[role="tabpanel"] .scroll-view__viewport').first()
217
+ const pop = viewer.locator('[data-slot="line-comment-popover"][data-inline-body]').first()
218
+ const tools = viewer.locator('[data-slot="line-comment-tools"]').first()
219
+
220
+ const [width, viewBox, popBox, toolsBox] = await Promise.all([
221
+ view.evaluate((el) => el.scrollWidth - el.clientWidth),
222
+ view.boundingBox(),
223
+ pop.boundingBox(),
224
+ tools.boundingBox(),
225
+ ])
226
+
227
+ if (!viewBox || !popBox || !toolsBox) return null
228
+
229
+ return {
230
+ width,
231
+ pop: popBox.x + popBox.width - (viewBox.x + viewBox.width),
232
+ tools: toolsBox.x + toolsBox.width - (viewBox.x + viewBox.width),
233
+ }
234
+ }
235
+
236
+ test("review applies inline comment clicks without horizontal overflow", async ({ page, withProject }) => {
237
+ test.setTimeout(180_000)
238
+
239
+ const tag = `review-comment-${Date.now()}`
240
+ const file = `review-comment-${tag}.txt`
241
+ const note = `comment ${tag}`
242
+
243
+ await page.setViewportSize({ width: 1280, height: 900 })
244
+
245
+ await withProject(async (project) => {
246
+ const sdk = createSdk(project.directory)
247
+
248
+ await withSession(sdk, `e2e review comment ${tag}`, async (session) => {
249
+ await patch(sdk, session.id, seed([{ file, mark: tag }]))
250
+
251
+ await expect
252
+ .poll(
253
+ async () => {
254
+ const diff = await sdk.session.diff({ sessionID: session.id }).then((res) => res.data ?? [])
255
+ return diff.length
256
+ },
257
+ { timeout: 60_000 },
258
+ )
259
+ .toBe(1)
260
+
261
+ await project.gotoSession(session.id)
262
+ await show(page)
263
+
264
+ const tab = page.getByRole("tab", { name: /Review/i }).first()
265
+ await expect(tab).toBeVisible()
266
+ await tab.click()
267
+
268
+ await expand(page)
269
+ await waitMark(page, file, tag)
270
+ await comment(page, file, note)
271
+
272
+ await expect
273
+ .poll(async () => (await overflow(page, file))?.width ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
274
+ .toBeLessThanOrEqual(1)
275
+ await expect
276
+ .poll(async () => (await overflow(page, file))?.pop ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
277
+ .toBeLessThanOrEqual(1)
278
+ await expect
279
+ .poll(async () => (await overflow(page, file))?.tools ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
280
+ .toBeLessThanOrEqual(1)
281
+ })
282
+ })
283
+ })
284
+
285
+ test("review file comments submit on click without clipping actions", async ({ page, withProject }) => {
286
+ test.setTimeout(180_000)
287
+
288
+ const tag = `review-file-comment-${Date.now()}`
289
+ const file = `review-file-comment-${tag}.txt`
290
+ const note = `comment ${tag}`
291
+
292
+ await page.setViewportSize({ width: 1280, height: 900 })
293
+
294
+ await withProject(async (project) => {
295
+ const sdk = createSdk(project.directory)
296
+
297
+ await withSession(sdk, `e2e review file comment ${tag}`, async (session) => {
298
+ await patch(sdk, session.id, seed([{ file, mark: tag }]))
299
+
300
+ await expect
301
+ .poll(
302
+ async () => {
303
+ const diff = await sdk.session.diff({ sessionID: session.id }).then((res) => res.data ?? [])
304
+ return diff.length
305
+ },
306
+ { timeout: 60_000 },
307
+ )
308
+ .toBe(1)
309
+
310
+ await project.gotoSession(session.id)
311
+ await show(page)
312
+
313
+ const tab = page.getByRole("tab", { name: /Review/i }).first()
314
+ await expect(tab).toBeVisible()
315
+ await tab.click()
316
+
317
+ await expand(page)
318
+ await waitMark(page, file, tag)
319
+ await openReviewFile(page, file)
320
+ await fileComment(page, note)
321
+
322
+ await expect
323
+ .poll(async () => (await fileOverflow(page))?.width ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
324
+ .toBeLessThanOrEqual(1)
325
+ await expect
326
+ .poll(async () => (await fileOverflow(page))?.pop ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
327
+ .toBeLessThanOrEqual(1)
328
+ await expect
329
+ .poll(async () => (await fileOverflow(page))?.tools ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
330
+ .toBeLessThanOrEqual(1)
331
+ })
332
+ })
333
+ })
334
+
335
+ test("review keeps scroll position after a live diff update", async ({ page, withProject }) => {
336
+ test.skip(Boolean(process.env.CI), "Flaky in CI for now.")
337
+ test.setTimeout(180_000)
338
+
339
+ const tag = `review-${Date.now()}`
340
+ const list = files(tag)
341
+ const hit = list[list.length - 4]!
342
+ const next = `${tag}-live`
343
+
344
+ await page.setViewportSize({ width: 1600, height: 1000 })
345
+
346
+ await withProject(async (project) => {
347
+ const sdk = createSdk(project.directory)
348
+
349
+ await withSession(sdk, `e2e review ${tag}`, async (session) => {
350
+ await patch(sdk, session.id, seed(list))
351
+
352
+ await expect
353
+ .poll(
354
+ async () => {
355
+ const info = await sdk.session.get({ sessionID: session.id }).then((res) => res.data)
356
+ return info?.summary?.files ?? 0
357
+ },
358
+ { timeout: 60_000 },
359
+ )
360
+ .toBe(list.length)
361
+
362
+ await expect
363
+ .poll(
364
+ async () => {
365
+ const diff = await sdk.session.diff({ sessionID: session.id }).then((res) => res.data ?? [])
366
+ return diff.length
367
+ },
368
+ { timeout: 60_000 },
369
+ )
370
+ .toBe(list.length)
371
+
372
+ await project.gotoSession(session.id)
373
+ await show(page)
374
+
375
+ const tab = page.getByRole("tab", { name: /Review/i }).first()
376
+ await expect(tab).toBeVisible()
377
+ await tab.click()
378
+
379
+ const view = page.locator('[data-slot="session-review-scroll"] .scroll-view__viewport').first()
380
+ await expect(view).toBeVisible()
381
+ const heads = page.getByRole("heading", { level: 3 }).filter({ hasText: /^review-scroll-/ })
382
+ await expect(heads).toHaveCount(list.length, {
383
+ timeout: 60_000,
384
+ })
385
+
386
+ await expand(page)
387
+ await waitMark(page, hit.file, hit.mark)
388
+
389
+ const row = page
390
+ .getByRole("heading", { level: 3, name: new RegExp(hit.file.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")) })
391
+ .first()
392
+ await expect(row).toBeVisible()
393
+ await row.evaluate((el) => el.scrollIntoView({ block: "center" }))
394
+
395
+ await expect.poll(async () => (await spot(page, hit.file))?.y ?? 0).toBeGreaterThan(200)
396
+ const prev = await spot(page, hit.file)
397
+ if (!prev) throw new Error(`missing review row for ${hit.file}`)
398
+
399
+ await patch(sdk, session.id, edit(hit.file, hit.mark, next))
400
+
401
+ await expect
402
+ .poll(
403
+ async () => {
404
+ const diff = await sdk.session.diff({ sessionID: session.id }).then((res) => res.data ?? [])
405
+ const item = diff.find((item) => item.file === hit.file)
406
+ return typeof item?.after === "string" ? item.after : ""
407
+ },
408
+ { timeout: 60_000 },
409
+ )
410
+ .toContain(`mark ${next}`)
411
+
412
+ await waitMark(page, hit.file, next)
413
+
414
+ await expect
415
+ .poll(
416
+ async () => {
417
+ const next = await spot(page, hit.file)
418
+ if (!next) return Number.POSITIVE_INFINITY
419
+ return Math.max(Math.abs(next.top - prev.top), Math.abs(next.y - prev.y))
420
+ },
421
+ { timeout: 60_000 },
422
+ )
423
+ .toBeLessThanOrEqual(32)
424
+ })
425
+ })
426
+ })
@@ -0,0 +1,233 @@
1
+ import type { Page } from "@playwright/test"
2
+ import { test, expect } from "../fixtures"
3
+ import { withSession } from "../actions"
4
+ import { createSdk, modKey } from "../utils"
5
+ import { promptSelector } from "../selectors"
6
+
7
+ async function seedConversation(input: {
8
+ page: Page
9
+ sdk: ReturnType<typeof createSdk>
10
+ sessionID: string
11
+ token: string
12
+ }) {
13
+ const messages = async () =>
14
+ await input.sdk.session.messages({ sessionID: input.sessionID, limit: 100 }).then((r) => r.data ?? [])
15
+ const seeded = await messages()
16
+ const userIDs = new Set(seeded.filter((m) => m.info.role === "user").map((m) => m.info.id))
17
+
18
+ const prompt = input.page.locator(promptSelector)
19
+ await expect(prompt).toBeVisible()
20
+ await input.sdk.session.promptAsync({
21
+ sessionID: input.sessionID,
22
+ noReply: true,
23
+ parts: [{ type: "text", text: input.token }],
24
+ })
25
+
26
+ let userMessageID: string | undefined
27
+ await expect
28
+ .poll(
29
+ async () => {
30
+ const users = (await messages()).filter(
31
+ (m) =>
32
+ !userIDs.has(m.info.id) &&
33
+ m.info.role === "user" &&
34
+ m.parts.filter((p) => p.type === "text").some((p) => p.text.includes(input.token)),
35
+ )
36
+ if (users.length === 0) return false
37
+
38
+ const user = users[users.length - 1]
39
+ if (!user) return false
40
+ userMessageID = user.info.id
41
+ return true
42
+ },
43
+ { timeout: 90_000, intervals: [250, 500, 1_000] },
44
+ )
45
+ .toBe(true)
46
+
47
+ if (!userMessageID) throw new Error("Expected a user message id")
48
+ await expect(input.page.locator(`[data-message-id="${userMessageID}"]`)).toHaveCount(1, { timeout: 30_000 })
49
+ return { prompt, userMessageID }
50
+ }
51
+
52
+ test("slash undo sets revert and restores prior prompt", async ({ page, withProject }) => {
53
+ test.setTimeout(120_000)
54
+
55
+ const token = `undo_${Date.now()}`
56
+
57
+ await withProject(async (project) => {
58
+ const sdk = createSdk(project.directory)
59
+
60
+ await withSession(sdk, `e2e undo ${Date.now()}`, async (session) => {
61
+ await project.gotoSession(session.id)
62
+
63
+ const seeded = await seedConversation({ page, sdk, sessionID: session.id, token })
64
+
65
+ await seeded.prompt.click()
66
+ await page.keyboard.type("/undo")
67
+
68
+ const undo = page.locator('[data-slash-id="session.undo"]').first()
69
+ await expect(undo).toBeVisible()
70
+ await page.keyboard.press("Enter")
71
+
72
+ await expect
73
+ .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
74
+ timeout: 30_000,
75
+ })
76
+ .toBe(seeded.userMessageID)
77
+
78
+ await expect(seeded.prompt).toContainText(token)
79
+ await expect(page.locator(`[data-message-id="${seeded.userMessageID}"]`)).toHaveCount(0)
80
+ })
81
+ })
82
+ })
83
+
84
+ test("slash redo clears revert and restores latest state", async ({ page, withProject }) => {
85
+ test.setTimeout(120_000)
86
+
87
+ const token = `redo_${Date.now()}`
88
+
89
+ await withProject(async (project) => {
90
+ const sdk = createSdk(project.directory)
91
+
92
+ await withSession(sdk, `e2e redo ${Date.now()}`, async (session) => {
93
+ await project.gotoSession(session.id)
94
+
95
+ const seeded = await seedConversation({ page, sdk, sessionID: session.id, token })
96
+
97
+ await seeded.prompt.click()
98
+ await page.keyboard.type("/undo")
99
+
100
+ const undo = page.locator('[data-slash-id="session.undo"]').first()
101
+ await expect(undo).toBeVisible()
102
+ await page.keyboard.press("Enter")
103
+
104
+ await expect
105
+ .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
106
+ timeout: 30_000,
107
+ })
108
+ .toBe(seeded.userMessageID)
109
+
110
+ await seeded.prompt.click()
111
+ await page.keyboard.press(`${modKey}+A`)
112
+ await page.keyboard.press("Backspace")
113
+ await page.keyboard.type("/redo")
114
+
115
+ const redo = page.locator('[data-slash-id="session.redo"]').first()
116
+ await expect(redo).toBeVisible()
117
+ await page.keyboard.press("Enter")
118
+
119
+ await expect
120
+ .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
121
+ timeout: 30_000,
122
+ })
123
+ .toBeUndefined()
124
+
125
+ await expect(seeded.prompt).not.toContainText(token)
126
+ await expect(page.locator(`[data-message-id="${seeded.userMessageID}"]`)).toHaveCount(1)
127
+ })
128
+ })
129
+ })
130
+
131
+ test("slash undo/redo traverses multi-step revert stack", async ({ page, withProject }) => {
132
+ test.setTimeout(120_000)
133
+
134
+ const firstToken = `undo_redo_first_${Date.now()}`
135
+ const secondToken = `undo_redo_second_${Date.now()}`
136
+
137
+ await withProject(async (project) => {
138
+ const sdk = createSdk(project.directory)
139
+
140
+ await withSession(sdk, `e2e undo redo stack ${Date.now()}`, async (session) => {
141
+ await project.gotoSession(session.id)
142
+
143
+ const first = await seedConversation({
144
+ page,
145
+ sdk,
146
+ sessionID: session.id,
147
+ token: firstToken,
148
+ })
149
+ const second = await seedConversation({
150
+ page,
151
+ sdk,
152
+ sessionID: session.id,
153
+ token: secondToken,
154
+ })
155
+
156
+ expect(first.userMessageID).not.toBe(second.userMessageID)
157
+
158
+ const firstMessage = page.locator(`[data-message-id="${first.userMessageID}"]`)
159
+ const secondMessage = page.locator(`[data-message-id="${second.userMessageID}"]`)
160
+
161
+ await expect(firstMessage).toHaveCount(1)
162
+ await expect(secondMessage).toHaveCount(1)
163
+
164
+ await second.prompt.click()
165
+ await page.keyboard.press(`${modKey}+A`)
166
+ await page.keyboard.press("Backspace")
167
+ await page.keyboard.type("/undo")
168
+
169
+ const undo = page.locator('[data-slash-id="session.undo"]').first()
170
+ await expect(undo).toBeVisible()
171
+ await page.keyboard.press("Enter")
172
+
173
+ await expect
174
+ .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
175
+ timeout: 30_000,
176
+ })
177
+ .toBe(second.userMessageID)
178
+
179
+ await expect(firstMessage).toHaveCount(1)
180
+ await expect(secondMessage).toHaveCount(0)
181
+
182
+ await second.prompt.click()
183
+ await page.keyboard.press(`${modKey}+A`)
184
+ await page.keyboard.press("Backspace")
185
+ await page.keyboard.type("/undo")
186
+ await expect(undo).toBeVisible()
187
+ await page.keyboard.press("Enter")
188
+
189
+ await expect
190
+ .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
191
+ timeout: 30_000,
192
+ })
193
+ .toBe(first.userMessageID)
194
+
195
+ await expect(firstMessage).toHaveCount(0)
196
+ await expect(secondMessage).toHaveCount(0)
197
+
198
+ await second.prompt.click()
199
+ await page.keyboard.press(`${modKey}+A`)
200
+ await page.keyboard.press("Backspace")
201
+ await page.keyboard.type("/redo")
202
+
203
+ const redo = page.locator('[data-slash-id="session.redo"]').first()
204
+ await expect(redo).toBeVisible()
205
+ await page.keyboard.press("Enter")
206
+
207
+ await expect
208
+ .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
209
+ timeout: 30_000,
210
+ })
211
+ .toBe(second.userMessageID)
212
+
213
+ await expect(firstMessage).toHaveCount(1)
214
+ await expect(secondMessage).toHaveCount(0)
215
+
216
+ await second.prompt.click()
217
+ await page.keyboard.press(`${modKey}+A`)
218
+ await page.keyboard.press("Backspace")
219
+ await page.keyboard.type("/redo")
220
+ await expect(redo).toBeVisible()
221
+ await page.keyboard.press("Enter")
222
+
223
+ await expect
224
+ .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
225
+ timeout: 30_000,
226
+ })
227
+ .toBeUndefined()
228
+
229
+ await expect(firstMessage).toHaveCount(1)
230
+ await expect(secondMessage).toHaveCount(1)
231
+ })
232
+ })
233
+ })