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,136 @@
1
+ import { test, expect } from "../fixtures"
2
+ import { closeDialog, openSettings } from "../actions"
3
+
4
+ test("custom provider form can be filled and validates input", async ({ page, gotoSession }) => {
5
+ await gotoSession()
6
+
7
+ const settings = await openSettings(page)
8
+ await settings.getByRole("tab", { name: "Providers" }).click()
9
+
10
+ const customProviderSection = settings.locator('[data-component="custom-provider-section"]')
11
+ await expect(customProviderSection).toBeVisible()
12
+
13
+ const connectButton = customProviderSection.getByRole("button", { name: "Connect" })
14
+ await connectButton.click()
15
+
16
+ const providerDialog = page.getByRole("dialog").filter({ has: page.getByText("Custom provider") })
17
+ await expect(providerDialog).toBeVisible()
18
+
19
+ await providerDialog.getByLabel("Provider ID").fill("test-provider")
20
+ await providerDialog.getByLabel("Display name").fill("Test Provider")
21
+ await providerDialog.getByLabel("Base URL").fill("http://localhost:9999/fake")
22
+ await providerDialog.getByLabel("API key").fill("fake-key")
23
+
24
+ await providerDialog.getByPlaceholder("model-id").first().fill("test-model")
25
+ await providerDialog.getByPlaceholder("Display Name").first().fill("Test Model")
26
+
27
+ await expect(providerDialog.getByRole("textbox", { name: "Provider ID" })).toHaveValue("test-provider")
28
+ await expect(providerDialog.getByRole("textbox", { name: "Display name" })).toHaveValue("Test Provider")
29
+ await expect(providerDialog.getByRole("textbox", { name: "Base URL" })).toHaveValue("http://localhost:9999/fake")
30
+ await expect(providerDialog.getByRole("textbox", { name: "API key" })).toHaveValue("fake-key")
31
+ await expect(providerDialog.getByPlaceholder("model-id").first()).toHaveValue("test-model")
32
+ await expect(providerDialog.getByPlaceholder("Display Name").first()).toHaveValue("Test Model")
33
+
34
+ await page.keyboard.press("Escape")
35
+ await expect(providerDialog).toHaveCount(0)
36
+
37
+ await closeDialog(page, settings)
38
+ })
39
+
40
+ test("custom provider form shows validation errors", async ({ page, gotoSession }) => {
41
+ await gotoSession()
42
+
43
+ const settings = await openSettings(page)
44
+ await settings.getByRole("tab", { name: "Providers" }).click()
45
+
46
+ const customProviderSection = settings.locator('[data-component="custom-provider-section"]')
47
+ await customProviderSection.getByRole("button", { name: "Connect" }).click()
48
+
49
+ const providerDialog = page.getByRole("dialog").filter({ has: page.getByText("Custom provider") })
50
+ await expect(providerDialog).toBeVisible()
51
+
52
+ await providerDialog.getByLabel("Provider ID").fill("invalid provider id")
53
+ await providerDialog.getByLabel("Base URL").fill("not-a-url")
54
+
55
+ await providerDialog.getByRole("button", { name: /submit|save/i }).click()
56
+
57
+ await expect(providerDialog.locator('[data-slot="input-error"]').filter({ hasText: /lowercase/i })).toBeVisible()
58
+ await expect(providerDialog.locator('[data-slot="input-error"]').filter({ hasText: /http/i })).toBeVisible()
59
+
60
+ await page.keyboard.press("Escape")
61
+ await expect(providerDialog).toHaveCount(0)
62
+
63
+ await closeDialog(page, settings)
64
+ })
65
+
66
+ test("custom provider form can add and remove models", async ({ page, gotoSession }) => {
67
+ await gotoSession()
68
+
69
+ const settings = await openSettings(page)
70
+ await settings.getByRole("tab", { name: "Providers" }).click()
71
+
72
+ const customProviderSection = settings.locator('[data-component="custom-provider-section"]')
73
+ await customProviderSection.getByRole("button", { name: "Connect" }).click()
74
+
75
+ const providerDialog = page.getByRole("dialog").filter({ has: page.getByText("Custom provider") })
76
+ await expect(providerDialog).toBeVisible()
77
+
78
+ await providerDialog.getByLabel("Provider ID").fill("multi-model-test")
79
+ await providerDialog.getByLabel("Display name").fill("Multi Model Test")
80
+ await providerDialog.getByLabel("Base URL").fill("http://localhost:9999/multi")
81
+
82
+ await providerDialog.getByPlaceholder("model-id").first().fill("model-1")
83
+ await providerDialog.getByPlaceholder("Display Name").first().fill("Model 1")
84
+
85
+ const idInputsBefore = await providerDialog.getByPlaceholder("model-id").count()
86
+ await providerDialog.getByRole("button", { name: "Add model" }).click()
87
+ const idInputsAfter = await providerDialog.getByPlaceholder("model-id").count()
88
+ expect(idInputsAfter).toBe(idInputsBefore + 1)
89
+
90
+ await providerDialog.getByPlaceholder("model-id").nth(1).fill("model-2")
91
+ await providerDialog.getByPlaceholder("Display Name").nth(1).fill("Model 2")
92
+
93
+ await expect(providerDialog.getByPlaceholder("model-id").nth(1)).toHaveValue("model-2")
94
+ await expect(providerDialog.getByPlaceholder("Display Name").nth(1)).toHaveValue("Model 2")
95
+
96
+ await page.keyboard.press("Escape")
97
+ await expect(providerDialog).toHaveCount(0)
98
+
99
+ await closeDialog(page, settings)
100
+ })
101
+
102
+ test("custom provider form can add and remove headers", async ({ page, gotoSession }) => {
103
+ await gotoSession()
104
+
105
+ const settings = await openSettings(page)
106
+ await settings.getByRole("tab", { name: "Providers" }).click()
107
+
108
+ const customProviderSection = settings.locator('[data-component="custom-provider-section"]')
109
+ await customProviderSection.getByRole("button", { name: "Connect" }).click()
110
+
111
+ const providerDialog = page.getByRole("dialog").filter({ has: page.getByText("Custom provider") })
112
+ await expect(providerDialog).toBeVisible()
113
+
114
+ await providerDialog.getByLabel("Provider ID").fill("header-test")
115
+ await providerDialog.getByLabel("Display name").fill("Header Test")
116
+ await providerDialog.getByLabel("Base URL").fill("http://localhost:9999/headers")
117
+
118
+ await providerDialog.getByPlaceholder("model-id").first().fill("model-x")
119
+ await providerDialog.getByPlaceholder("Display Name").first().fill("Model X")
120
+
121
+ const headerInputsBefore = await providerDialog.getByPlaceholder("Header-Name").count()
122
+ await providerDialog.getByRole("button", { name: "Add header" }).click()
123
+ const headerInputsAfter = await providerDialog.getByPlaceholder("Header-Name").count()
124
+ expect(headerInputsAfter).toBe(headerInputsBefore + 1)
125
+
126
+ await providerDialog.getByPlaceholder("Header-Name").first().fill("Authorization")
127
+ await providerDialog.getByPlaceholder("value").first().fill("Bearer token123")
128
+
129
+ await expect(providerDialog.getByPlaceholder("Header-Name").first()).toHaveValue("Authorization")
130
+ await expect(providerDialog.getByPlaceholder("value").first()).toHaveValue("Bearer token123")
131
+
132
+ await page.keyboard.press("Escape")
133
+ await expect(providerDialog).toHaveCount(0)
134
+
135
+ await closeDialog(page, settings)
136
+ })
@@ -0,0 +1,519 @@
1
+ import { test, expect, settingsKey } from "../fixtures"
2
+ import { closeDialog, openSettings } from "../actions"
3
+ import {
4
+ settingsColorSchemeSelector,
5
+ settingsFontSelector,
6
+ settingsLanguageSelectSelector,
7
+ settingsNotificationsAgentSelector,
8
+ settingsNotificationsErrorsSelector,
9
+ settingsNotificationsPermissionsSelector,
10
+ settingsReleaseNotesSelector,
11
+ settingsSoundsAgentSelector,
12
+ settingsSoundsErrorsSelector,
13
+ settingsSoundsPermissionsSelector,
14
+ settingsThemeSelector,
15
+ settingsUpdatesStartupSelector,
16
+ } from "../selectors"
17
+
18
+ test("smoke settings dialog opens, switches tabs, closes", async ({ page, gotoSession }) => {
19
+ await gotoSession()
20
+
21
+ const dialog = await openSettings(page)
22
+
23
+ await dialog.getByRole("tab", { name: "Shortcuts" }).click()
24
+ await expect(dialog.getByRole("button", { name: "Reset to defaults" })).toBeVisible()
25
+ await expect(dialog.getByPlaceholder("Search shortcuts")).toBeVisible()
26
+
27
+ await closeDialog(page, dialog)
28
+ })
29
+
30
+ test("changing language updates settings labels", async ({ page, gotoSession }) => {
31
+ await page.addInitScript(() => {
32
+ localStorage.setItem("opencode.global.dat:language", JSON.stringify({ locale: "en" }))
33
+ })
34
+
35
+ await gotoSession()
36
+
37
+ const dialog = await openSettings(page)
38
+
39
+ const heading = dialog.getByRole("heading", { level: 2 })
40
+ await expect(heading).toHaveText("General")
41
+
42
+ const select = dialog.locator(settingsLanguageSelectSelector)
43
+ await expect(select).toBeVisible()
44
+ await select.locator('[data-slot="select-select-trigger"]').click()
45
+
46
+ await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Deutsch" }).click()
47
+
48
+ await expect(heading).toHaveText("Allgemein")
49
+
50
+ await select.locator('[data-slot="select-select-trigger"]').click()
51
+ await page.locator('[data-slot="select-select-item"]').filter({ hasText: "English" }).click()
52
+ await expect(heading).toHaveText("General")
53
+ })
54
+
55
+ test("changing color scheme persists in localStorage", async ({ page, gotoSession }) => {
56
+ await gotoSession()
57
+
58
+ const dialog = await openSettings(page)
59
+ const select = dialog.locator(settingsColorSchemeSelector)
60
+ await expect(select).toBeVisible()
61
+
62
+ await select.locator('[data-slot="select-select-trigger"]').click()
63
+ await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Dark" }).click()
64
+
65
+ const colorScheme = await page.evaluate(() => {
66
+ return document.documentElement.getAttribute("data-color-scheme")
67
+ })
68
+ expect(colorScheme).toBe("dark")
69
+
70
+ await select.locator('[data-slot="select-select-trigger"]').click()
71
+ await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Light" }).click()
72
+
73
+ const lightColorScheme = await page.evaluate(() => {
74
+ return document.documentElement.getAttribute("data-color-scheme")
75
+ })
76
+ expect(lightColorScheme).toBe("light")
77
+ })
78
+
79
+ test("changing theme persists in localStorage", async ({ page, gotoSession }) => {
80
+ await gotoSession()
81
+
82
+ const dialog = await openSettings(page)
83
+ const select = dialog.locator(settingsThemeSelector)
84
+ await expect(select).toBeVisible()
85
+
86
+ const currentThemeId = await page.evaluate(() => {
87
+ return document.documentElement.getAttribute("data-theme")
88
+ })
89
+ const currentTheme = (await select.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? ""
90
+
91
+ await select.locator('[data-slot="select-select-trigger"]').click()
92
+
93
+ const items = page.locator('[data-slot="select-select-item"]')
94
+ const count = await items.count()
95
+ expect(count).toBeGreaterThan(1)
96
+
97
+ const nextTheme = (await items.locator('[data-slot="select-select-item-label"]').allTextContents())
98
+ .map((x) => x.trim())
99
+ .find((x) => x && x !== currentTheme)
100
+ expect(nextTheme).toBeTruthy()
101
+
102
+ await items.filter({ hasText: nextTheme! }).first().click()
103
+
104
+ await page.keyboard.press("Escape")
105
+
106
+ const storedThemeId = await page.evaluate(() => {
107
+ return localStorage.getItem("opencode-theme-id")
108
+ })
109
+
110
+ expect(storedThemeId).not.toBeNull()
111
+ expect(storedThemeId).not.toBe(currentThemeId)
112
+
113
+ const dataTheme = await page.evaluate(() => {
114
+ return document.documentElement.getAttribute("data-theme")
115
+ })
116
+ expect(dataTheme).toBe(storedThemeId)
117
+ })
118
+
119
+ test("legacy oc-1 theme migrates to oc-2", async ({ page, gotoSession }) => {
120
+ await page.addInitScript(() => {
121
+ localStorage.setItem("opencode-theme-id", "oc-1")
122
+ localStorage.setItem("opencode-theme-css-light", "--background-base:#fff;")
123
+ localStorage.setItem("opencode-theme-css-dark", "--background-base:#000;")
124
+ })
125
+
126
+ await gotoSession()
127
+
128
+ await expect(page.locator("html")).toHaveAttribute("data-theme", "oc-2")
129
+
130
+ await expect
131
+ .poll(async () => {
132
+ return await page.evaluate(() => {
133
+ return localStorage.getItem("opencode-theme-id")
134
+ })
135
+ })
136
+ .toBe("oc-2")
137
+
138
+ await expect
139
+ .poll(async () => {
140
+ return await page.evaluate(() => {
141
+ return localStorage.getItem("opencode-theme-css-light")
142
+ })
143
+ })
144
+ .toBeNull()
145
+
146
+ await expect
147
+ .poll(async () => {
148
+ return await page.evaluate(() => {
149
+ return localStorage.getItem("opencode-theme-css-dark")
150
+ })
151
+ })
152
+ .toBeNull()
153
+ })
154
+
155
+ test("changing font persists in localStorage and updates CSS variable", async ({ page, gotoSession }) => {
156
+ await gotoSession()
157
+
158
+ const dialog = await openSettings(page)
159
+ const select = dialog.locator(settingsFontSelector)
160
+ await expect(select).toBeVisible()
161
+
162
+ const initialFontFamily = await page.evaluate(() => {
163
+ return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono")
164
+ })
165
+ expect(initialFontFamily).toContain("IBM Plex Mono")
166
+
167
+ await select.locator('[data-slot="select-select-trigger"]').click()
168
+
169
+ const items = page.locator('[data-slot="select-select-item"]')
170
+ await items.nth(2).click()
171
+
172
+ await page.waitForTimeout(100)
173
+
174
+ const stored = await page.evaluate((key) => {
175
+ const raw = localStorage.getItem(key)
176
+ return raw ? JSON.parse(raw) : null
177
+ }, settingsKey)
178
+
179
+ expect(stored?.appearance?.font).not.toBe("ibm-plex-mono")
180
+
181
+ const newFontFamily = await page.evaluate(() => {
182
+ return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono")
183
+ })
184
+ expect(newFontFamily).not.toBe(initialFontFamily)
185
+ })
186
+
187
+ test("color scheme and font rehydrate after reload", async ({ page, gotoSession }) => {
188
+ await gotoSession()
189
+
190
+ const dialog = await openSettings(page)
191
+
192
+ const colorSchemeSelect = dialog.locator(settingsColorSchemeSelector)
193
+ await expect(colorSchemeSelect).toBeVisible()
194
+ await colorSchemeSelect.locator('[data-slot="select-select-trigger"]').click()
195
+ await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Dark" }).click()
196
+ await expect(page.locator("html")).toHaveAttribute("data-color-scheme", "dark")
197
+
198
+ const fontSelect = dialog.locator(settingsFontSelector)
199
+ await expect(fontSelect).toBeVisible()
200
+
201
+ const initialFontFamily = await page.evaluate(() => {
202
+ return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim()
203
+ })
204
+
205
+ const initialSettings = await page.evaluate((key) => {
206
+ const raw = localStorage.getItem(key)
207
+ return raw ? JSON.parse(raw) : null
208
+ }, settingsKey)
209
+
210
+ const currentFont =
211
+ (await fontSelect.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? ""
212
+ await fontSelect.locator('[data-slot="select-select-trigger"]').click()
213
+
214
+ const fontItems = page.locator('[data-slot="select-select-item"]')
215
+ expect(await fontItems.count()).toBeGreaterThan(1)
216
+
217
+ if (currentFont) {
218
+ await fontItems.filter({ hasNotText: currentFont }).first().click()
219
+ }
220
+ if (!currentFont) {
221
+ await fontItems.nth(1).click()
222
+ }
223
+
224
+ await expect
225
+ .poll(async () => {
226
+ return await page.evaluate((key) => {
227
+ const raw = localStorage.getItem(key)
228
+ return raw ? JSON.parse(raw) : null
229
+ }, settingsKey)
230
+ })
231
+ .toMatchObject({
232
+ appearance: {
233
+ font: expect.any(String),
234
+ },
235
+ })
236
+
237
+ const updatedSettings = await page.evaluate((key) => {
238
+ const raw = localStorage.getItem(key)
239
+ return raw ? JSON.parse(raw) : null
240
+ }, settingsKey)
241
+
242
+ const updatedFontFamily = await page.evaluate(() => {
243
+ return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim()
244
+ })
245
+ expect(updatedFontFamily).not.toBe(initialFontFamily)
246
+ expect(updatedSettings?.appearance?.font).not.toBe(initialSettings?.appearance?.font)
247
+
248
+ await closeDialog(page, dialog)
249
+ await page.reload()
250
+
251
+ await expect(page.locator("html")).toHaveAttribute("data-color-scheme", "dark")
252
+
253
+ await expect
254
+ .poll(async () => {
255
+ return await page.evaluate((key) => {
256
+ const raw = localStorage.getItem(key)
257
+ return raw ? JSON.parse(raw) : null
258
+ }, settingsKey)
259
+ })
260
+ .toMatchObject({
261
+ appearance: {
262
+ font: updatedSettings?.appearance?.font,
263
+ },
264
+ })
265
+
266
+ const rehydratedSettings = await page.evaluate((key) => {
267
+ const raw = localStorage.getItem(key)
268
+ return raw ? JSON.parse(raw) : null
269
+ }, settingsKey)
270
+
271
+ await expect
272
+ .poll(async () => {
273
+ return await page.evaluate(() => {
274
+ return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim()
275
+ })
276
+ })
277
+ .not.toBe(initialFontFamily)
278
+
279
+ const rehydratedFontFamily = await page.evaluate(() => {
280
+ return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim()
281
+ })
282
+ expect(rehydratedFontFamily).not.toBe(initialFontFamily)
283
+ expect(rehydratedSettings?.appearance?.font).toBe(updatedSettings?.appearance?.font)
284
+ })
285
+
286
+ test("toggling notification agent switch updates localStorage", async ({ page, gotoSession }) => {
287
+ await gotoSession()
288
+
289
+ const dialog = await openSettings(page)
290
+ const switchContainer = dialog.locator(settingsNotificationsAgentSelector)
291
+ await expect(switchContainer).toBeVisible()
292
+
293
+ const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
294
+ const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
295
+ expect(initialState).toBe(true)
296
+
297
+ await switchContainer.locator('[data-slot="switch-control"]').click()
298
+ await page.waitForTimeout(100)
299
+
300
+ const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
301
+ expect(newState).toBe(false)
302
+
303
+ const stored = await page.evaluate((key) => {
304
+ const raw = localStorage.getItem(key)
305
+ return raw ? JSON.parse(raw) : null
306
+ }, settingsKey)
307
+
308
+ expect(stored?.notifications?.agent).toBe(false)
309
+ })
310
+
311
+ test("toggling notification permissions switch updates localStorage", async ({ page, gotoSession }) => {
312
+ await gotoSession()
313
+
314
+ const dialog = await openSettings(page)
315
+ const switchContainer = dialog.locator(settingsNotificationsPermissionsSelector)
316
+ await expect(switchContainer).toBeVisible()
317
+
318
+ const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
319
+ const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
320
+ expect(initialState).toBe(true)
321
+
322
+ await switchContainer.locator('[data-slot="switch-control"]').click()
323
+ await page.waitForTimeout(100)
324
+
325
+ const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
326
+ expect(newState).toBe(false)
327
+
328
+ const stored = await page.evaluate((key) => {
329
+ const raw = localStorage.getItem(key)
330
+ return raw ? JSON.parse(raw) : null
331
+ }, settingsKey)
332
+
333
+ expect(stored?.notifications?.permissions).toBe(false)
334
+ })
335
+
336
+ test("toggling notification errors switch updates localStorage", async ({ page, gotoSession }) => {
337
+ await gotoSession()
338
+
339
+ const dialog = await openSettings(page)
340
+ const switchContainer = dialog.locator(settingsNotificationsErrorsSelector)
341
+ await expect(switchContainer).toBeVisible()
342
+
343
+ const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
344
+ const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
345
+ expect(initialState).toBe(false)
346
+
347
+ await switchContainer.locator('[data-slot="switch-control"]').click()
348
+ await page.waitForTimeout(100)
349
+
350
+ const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
351
+ expect(newState).toBe(true)
352
+
353
+ const stored = await page.evaluate((key) => {
354
+ const raw = localStorage.getItem(key)
355
+ return raw ? JSON.parse(raw) : null
356
+ }, settingsKey)
357
+
358
+ expect(stored?.notifications?.errors).toBe(true)
359
+ })
360
+
361
+ test("changing sound agent selection persists in localStorage", async ({ page, gotoSession }) => {
362
+ await gotoSession()
363
+
364
+ const dialog = await openSettings(page)
365
+ const select = dialog.locator(settingsSoundsAgentSelector)
366
+ await expect(select).toBeVisible()
367
+
368
+ await select.locator('[data-slot="select-select-trigger"]').click()
369
+
370
+ const items = page.locator('[data-slot="select-select-item"]')
371
+ await items.nth(2).click()
372
+
373
+ const stored = await page.evaluate((key) => {
374
+ const raw = localStorage.getItem(key)
375
+ return raw ? JSON.parse(raw) : null
376
+ }, settingsKey)
377
+
378
+ expect(stored?.sounds?.agent).not.toBe("staplebops-01")
379
+ })
380
+
381
+ test("selecting none disables agent sound", async ({ page, gotoSession }) => {
382
+ await gotoSession()
383
+
384
+ const dialog = await openSettings(page)
385
+ const select = dialog.locator(settingsSoundsAgentSelector)
386
+ const trigger = select.locator('[data-slot="select-select-trigger"]')
387
+ await expect(select).toBeVisible()
388
+ await expect(trigger).toBeEnabled()
389
+
390
+ await trigger.click()
391
+ const items = page.locator('[data-slot="select-select-item"]')
392
+ await expect(items.first()).toBeVisible()
393
+ await items.first().click()
394
+
395
+ const stored = await page.evaluate((key) => {
396
+ const raw = localStorage.getItem(key)
397
+ return raw ? JSON.parse(raw) : null
398
+ }, settingsKey)
399
+
400
+ expect(stored?.sounds?.agentEnabled).toBe(false)
401
+ })
402
+
403
+ test("changing permissions and errors sounds updates localStorage", async ({ page, gotoSession }) => {
404
+ await gotoSession()
405
+
406
+ const dialog = await openSettings(page)
407
+ const permissionsSelect = dialog.locator(settingsSoundsPermissionsSelector)
408
+ const errorsSelect = dialog.locator(settingsSoundsErrorsSelector)
409
+ await expect(permissionsSelect).toBeVisible()
410
+ await expect(errorsSelect).toBeVisible()
411
+
412
+ const initial = await page.evaluate((key) => {
413
+ const raw = localStorage.getItem(key)
414
+ return raw ? JSON.parse(raw) : null
415
+ }, settingsKey)
416
+
417
+ const permissionsCurrent =
418
+ (await permissionsSelect.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? ""
419
+ await permissionsSelect.locator('[data-slot="select-select-trigger"]').click()
420
+ const permissionItems = page.locator('[data-slot="select-select-item"]')
421
+ expect(await permissionItems.count()).toBeGreaterThan(1)
422
+ if (permissionsCurrent) {
423
+ await permissionItems.filter({ hasNotText: permissionsCurrent }).first().click()
424
+ }
425
+ if (!permissionsCurrent) {
426
+ await permissionItems.nth(1).click()
427
+ }
428
+
429
+ const errorsCurrent =
430
+ (await errorsSelect.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? ""
431
+ await errorsSelect.locator('[data-slot="select-select-trigger"]').click()
432
+ const errorItems = page.locator('[data-slot="select-select-item"]')
433
+ expect(await errorItems.count()).toBeGreaterThan(1)
434
+ if (errorsCurrent) {
435
+ await errorItems.filter({ hasNotText: errorsCurrent }).first().click()
436
+ }
437
+ if (!errorsCurrent) {
438
+ await errorItems.nth(1).click()
439
+ }
440
+
441
+ await expect
442
+ .poll(async () => {
443
+ return await page.evaluate((key) => {
444
+ const raw = localStorage.getItem(key)
445
+ return raw ? JSON.parse(raw) : null
446
+ }, settingsKey)
447
+ })
448
+ .toMatchObject({
449
+ sounds: {
450
+ permissions: expect.any(String),
451
+ errors: expect.any(String),
452
+ },
453
+ })
454
+
455
+ const stored = await page.evaluate((key) => {
456
+ const raw = localStorage.getItem(key)
457
+ return raw ? JSON.parse(raw) : null
458
+ }, settingsKey)
459
+
460
+ expect(stored?.sounds?.permissions).not.toBe(initial?.sounds?.permissions)
461
+ expect(stored?.sounds?.errors).not.toBe(initial?.sounds?.errors)
462
+ })
463
+
464
+ test("toggling updates startup switch updates localStorage", async ({ page, gotoSession }) => {
465
+ await gotoSession()
466
+
467
+ const dialog = await openSettings(page)
468
+ const switchContainer = dialog.locator(settingsUpdatesStartupSelector)
469
+ await expect(switchContainer).toBeVisible()
470
+
471
+ const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
472
+
473
+ const isDisabled = await toggleInput.evaluate((el: HTMLInputElement) => el.disabled)
474
+ if (isDisabled) {
475
+ test.skip()
476
+ return
477
+ }
478
+
479
+ const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
480
+ expect(initialState).toBe(true)
481
+
482
+ await switchContainer.locator('[data-slot="switch-control"]').click()
483
+ await page.waitForTimeout(100)
484
+
485
+ const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
486
+ expect(newState).toBe(false)
487
+
488
+ const stored = await page.evaluate((key) => {
489
+ const raw = localStorage.getItem(key)
490
+ return raw ? JSON.parse(raw) : null
491
+ }, settingsKey)
492
+
493
+ expect(stored?.updates?.startup).toBe(false)
494
+ })
495
+
496
+ test("toggling release notes switch updates localStorage", async ({ page, gotoSession }) => {
497
+ await gotoSession()
498
+
499
+ const dialog = await openSettings(page)
500
+ const switchContainer = dialog.locator(settingsReleaseNotesSelector)
501
+ await expect(switchContainer).toBeVisible()
502
+
503
+ const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
504
+ const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
505
+ expect(initialState).toBe(true)
506
+
507
+ await switchContainer.locator('[data-slot="switch-control"]').click()
508
+ await page.waitForTimeout(100)
509
+
510
+ const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
511
+ expect(newState).toBe(false)
512
+
513
+ const stored = await page.evaluate((key) => {
514
+ const raw = localStorage.getItem(key)
515
+ return raw ? JSON.parse(raw) : null
516
+ }, settingsKey)
517
+
518
+ expect(stored?.general?.releaseNotes).toBe(false)
519
+ })