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,530 @@
1
+ import { test, expect } from "../fixtures"
2
+ import {
3
+ composerEvent,
4
+ type ComposerDriverState,
5
+ type ComposerProbeState,
6
+ type ComposerWindow,
7
+ } from "../../src/testing/session-composer"
8
+ import { cleanupSession, clearSessionDockSeed, seedSessionQuestion } from "../actions"
9
+ import {
10
+ permissionDockSelector,
11
+ promptSelector,
12
+ questionDockSelector,
13
+ sessionComposerDockSelector,
14
+ sessionTodoToggleButtonSelector,
15
+ } from "../selectors"
16
+
17
+ type Sdk = Parameters<typeof clearSessionDockSeed>[0]
18
+ type PermissionRule = { permission: string; pattern: string; action: "allow" | "deny" | "ask" }
19
+
20
+ async function withDockSession<T>(
21
+ sdk: Sdk,
22
+ title: string,
23
+ fn: (session: { id: string; title: string }) => Promise<T>,
24
+ opts?: { permission?: PermissionRule[] },
25
+ ) {
26
+ const session = await sdk.session
27
+ .create(opts?.permission ? { title, permission: opts.permission } : { title })
28
+ .then((r) => r.data)
29
+ if (!session?.id) throw new Error("Session create did not return an id")
30
+ try {
31
+ return await fn(session)
32
+ } finally {
33
+ await cleanupSession({ sdk, sessionID: session.id })
34
+ }
35
+ }
36
+
37
+ test.setTimeout(120_000)
38
+
39
+ async function withDockSeed<T>(sdk: Sdk, sessionID: string, fn: () => Promise<T>) {
40
+ try {
41
+ return await fn()
42
+ } finally {
43
+ await clearSessionDockSeed(sdk, sessionID).catch(() => undefined)
44
+ }
45
+ }
46
+
47
+ async function clearPermissionDock(page: any, label: RegExp) {
48
+ const dock = page.locator(permissionDockSelector)
49
+ await expect(dock).toBeVisible()
50
+ await dock.getByRole("button", { name: label }).click()
51
+ }
52
+
53
+ async function setAutoAccept(page: any, enabled: boolean) {
54
+ const button = page.locator('[data-action="prompt-permissions"]').first()
55
+ await expect(button).toBeVisible()
56
+ const pressed = (await button.getAttribute("aria-pressed")) === "true"
57
+ if (pressed === enabled) return
58
+ await button.click()
59
+ await expect(button).toHaveAttribute("aria-pressed", enabled ? "true" : "false")
60
+ }
61
+
62
+ async function expectQuestionBlocked(page: any) {
63
+ await expect(page.locator(questionDockSelector)).toBeVisible()
64
+ await expect(page.locator(promptSelector)).toHaveCount(0)
65
+ }
66
+
67
+ async function expectQuestionOpen(page: any) {
68
+ await expect(page.locator(questionDockSelector)).toHaveCount(0)
69
+ await expect(page.locator(promptSelector)).toBeVisible()
70
+ }
71
+
72
+ async function expectPermissionBlocked(page: any) {
73
+ await expect(page.locator(permissionDockSelector)).toBeVisible()
74
+ await expect(page.locator(promptSelector)).toHaveCount(0)
75
+ }
76
+
77
+ async function expectPermissionOpen(page: any) {
78
+ await expect(page.locator(permissionDockSelector)).toHaveCount(0)
79
+ await expect(page.locator(promptSelector)).toBeVisible()
80
+ }
81
+
82
+ async function todoDock(page: any, sessionID: string) {
83
+ await page.addInitScript(() => {
84
+ const win = window as ComposerWindow
85
+ win.__opencode_e2e = {
86
+ ...win.__opencode_e2e,
87
+ composer: {
88
+ enabled: true,
89
+ sessions: {},
90
+ },
91
+ }
92
+ })
93
+
94
+ const write = async (driver: ComposerDriverState | undefined) => {
95
+ await page.evaluate(
96
+ (input) => {
97
+ const win = window as ComposerWindow
98
+ const composer = win.__opencode_e2e?.composer
99
+ if (!composer?.enabled) throw new Error("Composer e2e driver is not enabled")
100
+ composer.sessions ??= {}
101
+ const prev = composer.sessions[input.sessionID] ?? {}
102
+ if (!input.driver) {
103
+ if (!prev.probe) {
104
+ delete composer.sessions[input.sessionID]
105
+ } else {
106
+ composer.sessions[input.sessionID] = { probe: prev.probe }
107
+ }
108
+ } else {
109
+ composer.sessions[input.sessionID] = {
110
+ ...prev,
111
+ driver: input.driver,
112
+ }
113
+ }
114
+ window.dispatchEvent(new CustomEvent(input.event, { detail: { sessionID: input.sessionID } }))
115
+ },
116
+ { event: composerEvent, sessionID, driver },
117
+ )
118
+ }
119
+
120
+ const read = () =>
121
+ page.evaluate((sessionID) => {
122
+ const win = window as ComposerWindow
123
+ return win.__opencode_e2e?.composer?.sessions?.[sessionID]?.probe ?? null
124
+ }, sessionID) as Promise<ComposerProbeState | null>
125
+
126
+ const api = {
127
+ async clear() {
128
+ await write(undefined)
129
+ return api
130
+ },
131
+ async open(todos: NonNullable<ComposerDriverState["todos"]>) {
132
+ await write({ live: true, todos })
133
+ return api
134
+ },
135
+ async finish(todos: NonNullable<ComposerDriverState["todos"]>) {
136
+ await write({ live: false, todos })
137
+ return api
138
+ },
139
+ async expectOpen(states: ComposerProbeState["states"]) {
140
+ await expect.poll(read, { timeout: 10_000 }).toMatchObject({
141
+ mounted: true,
142
+ collapsed: false,
143
+ hidden: false,
144
+ count: states.length,
145
+ states,
146
+ })
147
+ return api
148
+ },
149
+ async expectCollapsed(states: ComposerProbeState["states"]) {
150
+ await expect.poll(read, { timeout: 10_000 }).toMatchObject({
151
+ mounted: true,
152
+ collapsed: true,
153
+ hidden: true,
154
+ count: states.length,
155
+ states,
156
+ })
157
+ return api
158
+ },
159
+ async expectClosed() {
160
+ await expect.poll(read, { timeout: 10_000 }).toMatchObject({ mounted: false })
161
+ return api
162
+ },
163
+ async collapse() {
164
+ await page.locator(sessionTodoToggleButtonSelector).click()
165
+ return api
166
+ },
167
+ async expand() {
168
+ await page.locator(sessionTodoToggleButtonSelector).click()
169
+ return api
170
+ },
171
+ }
172
+
173
+ return api
174
+ }
175
+
176
+ async function withMockPermission<T>(
177
+ page: any,
178
+ request: {
179
+ id: string
180
+ sessionID: string
181
+ permission: string
182
+ patterns: string[]
183
+ metadata?: Record<string, unknown>
184
+ always?: string[]
185
+ },
186
+ opts: { child?: any } | undefined,
187
+ fn: (state: { resolved: () => Promise<void> }) => Promise<T>,
188
+ ) {
189
+ let pending = [
190
+ {
191
+ ...request,
192
+ always: request.always ?? ["*"],
193
+ metadata: request.metadata ?? {},
194
+ },
195
+ ]
196
+
197
+ const list = async (route: any) => {
198
+ await route.fulfill({
199
+ status: 200,
200
+ contentType: "application/json",
201
+ body: JSON.stringify(pending),
202
+ })
203
+ }
204
+
205
+ const reply = async (route: any) => {
206
+ const url = new URL(route.request().url())
207
+ const id = url.pathname.split("/").pop()
208
+ pending = pending.filter((item) => item.id !== id)
209
+ await route.fulfill({
210
+ status: 200,
211
+ contentType: "application/json",
212
+ body: JSON.stringify(true),
213
+ })
214
+ }
215
+
216
+ await page.route("**/permission", list)
217
+ await page.route("**/session/*/permissions/*", reply)
218
+
219
+ const sessionList = opts?.child
220
+ ? async (route: any) => {
221
+ const res = await route.fetch()
222
+ const json = await res.json()
223
+ const list = Array.isArray(json) ? json : Array.isArray(json?.data) ? json.data : undefined
224
+ if (Array.isArray(list) && !list.some((item) => item?.id === opts.child?.id)) list.push(opts.child)
225
+ await route.fulfill({
226
+ status: res.status(),
227
+ headers: res.headers(),
228
+ contentType: "application/json",
229
+ body: JSON.stringify(json),
230
+ })
231
+ }
232
+ : undefined
233
+
234
+ if (sessionList) await page.route("**/session?*", sessionList)
235
+
236
+ const state = {
237
+ async resolved() {
238
+ await expect.poll(() => pending.length, { timeout: 10_000 }).toBe(0)
239
+ },
240
+ }
241
+
242
+ try {
243
+ return await fn(state)
244
+ } finally {
245
+ await page.unroute("**/permission", list)
246
+ await page.unroute("**/session/*/permissions/*", reply)
247
+ if (sessionList) await page.unroute("**/session?*", sessionList)
248
+ }
249
+ }
250
+
251
+ test("default dock shows prompt input", async ({ page, sdk, gotoSession }) => {
252
+ await withDockSession(sdk, "e2e composer dock default", async (session) => {
253
+ await gotoSession(session.id)
254
+
255
+ await expect(page.locator(sessionComposerDockSelector)).toBeVisible()
256
+ await expect(page.locator(promptSelector)).toBeVisible()
257
+ await expect(page.locator(questionDockSelector)).toHaveCount(0)
258
+ await expect(page.locator(permissionDockSelector)).toHaveCount(0)
259
+
260
+ await page.locator(promptSelector).click()
261
+ await expect(page.locator(promptSelector)).toBeFocused()
262
+ })
263
+ })
264
+
265
+ test("auto-accept toggle works before first submit", async ({ page, gotoSession }) => {
266
+ await gotoSession()
267
+
268
+ const button = page.locator('[data-action="prompt-permissions"]').first()
269
+ await expect(button).toBeVisible()
270
+ await expect(button).toHaveAttribute("aria-pressed", "false")
271
+
272
+ await setAutoAccept(page, true)
273
+ await setAutoAccept(page, false)
274
+ })
275
+
276
+ test("blocked question flow unblocks after submit", async ({ page, sdk, gotoSession }) => {
277
+ await withDockSession(sdk, "e2e composer dock question", async (session) => {
278
+ await withDockSeed(sdk, session.id, async () => {
279
+ await gotoSession(session.id)
280
+
281
+ await seedSessionQuestion(sdk, {
282
+ sessionID: session.id,
283
+ questions: [
284
+ {
285
+ header: "Need input",
286
+ question: "Pick one option",
287
+ options: [
288
+ { label: "Continue", description: "Continue now" },
289
+ { label: "Stop", description: "Stop here" },
290
+ ],
291
+ },
292
+ ],
293
+ })
294
+
295
+ const dock = page.locator(questionDockSelector)
296
+ await expectQuestionBlocked(page)
297
+
298
+ await dock.locator('[data-slot="question-option"]').first().click()
299
+ await dock.getByRole("button", { name: /submit/i }).click()
300
+
301
+ await expectQuestionOpen(page)
302
+ })
303
+ })
304
+ })
305
+
306
+ test("blocked permission flow supports allow once", async ({ page, sdk, gotoSession }) => {
307
+ await withDockSession(sdk, "e2e composer dock permission once", async (session) => {
308
+ await gotoSession(session.id)
309
+ await setAutoAccept(page, false)
310
+ await withMockPermission(
311
+ page,
312
+ {
313
+ id: "per_e2e_once",
314
+ sessionID: session.id,
315
+ permission: "bash",
316
+ patterns: ["/tmp/opencode-e2e-perm-once"],
317
+ metadata: { description: "Need permission for command" },
318
+ },
319
+ undefined,
320
+ async (state) => {
321
+ await page.goto(page.url())
322
+ await expectPermissionBlocked(page)
323
+
324
+ await clearPermissionDock(page, /allow once/i)
325
+ await state.resolved()
326
+ await page.goto(page.url())
327
+ await expectPermissionOpen(page)
328
+ },
329
+ )
330
+ })
331
+ })
332
+
333
+ test("blocked permission flow supports reject", async ({ page, sdk, gotoSession }) => {
334
+ await withDockSession(sdk, "e2e composer dock permission reject", async (session) => {
335
+ await gotoSession(session.id)
336
+ await setAutoAccept(page, false)
337
+ await withMockPermission(
338
+ page,
339
+ {
340
+ id: "per_e2e_reject",
341
+ sessionID: session.id,
342
+ permission: "bash",
343
+ patterns: ["/tmp/opencode-e2e-perm-reject"],
344
+ },
345
+ undefined,
346
+ async (state) => {
347
+ await page.goto(page.url())
348
+ await expectPermissionBlocked(page)
349
+
350
+ await clearPermissionDock(page, /deny/i)
351
+ await state.resolved()
352
+ await page.goto(page.url())
353
+ await expectPermissionOpen(page)
354
+ },
355
+ )
356
+ })
357
+ })
358
+
359
+ test("blocked permission flow supports allow always", async ({ page, sdk, gotoSession }) => {
360
+ await withDockSession(sdk, "e2e composer dock permission always", async (session) => {
361
+ await gotoSession(session.id)
362
+ await setAutoAccept(page, false)
363
+ await withMockPermission(
364
+ page,
365
+ {
366
+ id: "per_e2e_always",
367
+ sessionID: session.id,
368
+ permission: "bash",
369
+ patterns: ["/tmp/opencode-e2e-perm-always"],
370
+ metadata: { description: "Need permission for command" },
371
+ },
372
+ undefined,
373
+ async (state) => {
374
+ await page.goto(page.url())
375
+ await expectPermissionBlocked(page)
376
+
377
+ await clearPermissionDock(page, /allow always/i)
378
+ await state.resolved()
379
+ await page.goto(page.url())
380
+ await expectPermissionOpen(page)
381
+ },
382
+ )
383
+ })
384
+ })
385
+
386
+ test("child session question request blocks parent dock and unblocks after submit", async ({
387
+ page,
388
+ sdk,
389
+ gotoSession,
390
+ }) => {
391
+ await withDockSession(sdk, "e2e composer dock child question parent", async (session) => {
392
+ await gotoSession(session.id)
393
+
394
+ const child = await sdk.session
395
+ .create({
396
+ title: "e2e composer dock child question",
397
+ parentID: session.id,
398
+ })
399
+ .then((r) => r.data)
400
+ if (!child?.id) throw new Error("Child session create did not return an id")
401
+
402
+ try {
403
+ await withDockSeed(sdk, child.id, async () => {
404
+ await seedSessionQuestion(sdk, {
405
+ sessionID: child.id,
406
+ questions: [
407
+ {
408
+ header: "Child input",
409
+ question: "Pick one child option",
410
+ options: [
411
+ { label: "Continue", description: "Continue child" },
412
+ { label: "Stop", description: "Stop child" },
413
+ ],
414
+ },
415
+ ],
416
+ })
417
+
418
+ const dock = page.locator(questionDockSelector)
419
+ await expectQuestionBlocked(page)
420
+
421
+ await dock.locator('[data-slot="question-option"]').first().click()
422
+ await dock.getByRole("button", { name: /submit/i }).click()
423
+
424
+ await expectQuestionOpen(page)
425
+ })
426
+ } finally {
427
+ await cleanupSession({ sdk, sessionID: child.id })
428
+ }
429
+ })
430
+ })
431
+
432
+ test("child session permission request blocks parent dock and supports allow once", async ({
433
+ page,
434
+ sdk,
435
+ gotoSession,
436
+ }) => {
437
+ await withDockSession(sdk, "e2e composer dock child permission parent", async (session) => {
438
+ await gotoSession(session.id)
439
+ await setAutoAccept(page, false)
440
+
441
+ const child = await sdk.session
442
+ .create({
443
+ title: "e2e composer dock child permission",
444
+ parentID: session.id,
445
+ })
446
+ .then((r) => r.data)
447
+ if (!child?.id) throw new Error("Child session create did not return an id")
448
+
449
+ try {
450
+ await withMockPermission(
451
+ page,
452
+ {
453
+ id: "per_e2e_child",
454
+ sessionID: child.id,
455
+ permission: "bash",
456
+ patterns: ["/tmp/opencode-e2e-perm-child"],
457
+ metadata: { description: "Need child permission" },
458
+ },
459
+ { child },
460
+ async (state) => {
461
+ await page.goto(page.url())
462
+ await expectPermissionBlocked(page)
463
+
464
+ await clearPermissionDock(page, /allow once/i)
465
+ await state.resolved()
466
+ await page.goto(page.url())
467
+
468
+ await expectPermissionOpen(page)
469
+ },
470
+ )
471
+ } finally {
472
+ await cleanupSession({ sdk, sessionID: child.id })
473
+ }
474
+ })
475
+ })
476
+
477
+ test("todo dock transitions and collapse behavior", async ({ page, sdk, gotoSession }) => {
478
+ await withDockSession(sdk, "e2e composer dock todo", async (session) => {
479
+ const dock = await todoDock(page, session.id)
480
+ await gotoSession(session.id)
481
+ await expect(page.locator(sessionComposerDockSelector)).toBeVisible()
482
+
483
+ try {
484
+ await dock.open([
485
+ { content: "first task", status: "pending", priority: "high" },
486
+ { content: "second task", status: "in_progress", priority: "medium" },
487
+ ])
488
+ await dock.expectOpen(["pending", "in_progress"])
489
+
490
+ await dock.collapse()
491
+ await dock.expectCollapsed(["pending", "in_progress"])
492
+
493
+ await dock.expand()
494
+ await dock.expectOpen(["pending", "in_progress"])
495
+
496
+ await dock.finish([
497
+ { content: "first task", status: "completed", priority: "high" },
498
+ { content: "second task", status: "cancelled", priority: "medium" },
499
+ ])
500
+ await dock.expectClosed()
501
+ } finally {
502
+ await dock.clear()
503
+ }
504
+ })
505
+ })
506
+
507
+ test("keyboard focus stays off prompt while blocked", async ({ page, sdk, gotoSession }) => {
508
+ await withDockSession(sdk, "e2e composer dock keyboard", async (session) => {
509
+ await withDockSeed(sdk, session.id, async () => {
510
+ await gotoSession(session.id)
511
+
512
+ await seedSessionQuestion(sdk, {
513
+ sessionID: session.id,
514
+ questions: [
515
+ {
516
+ header: "Need input",
517
+ question: "Pick one option",
518
+ options: [{ label: "Continue", description: "Continue now" }],
519
+ },
520
+ ],
521
+ })
522
+
523
+ await expectQuestionBlocked(page)
524
+
525
+ await page.locator("main").click({ position: { x: 5, y: 5 } })
526
+ await page.keyboard.type("abc")
527
+ await expect(page.locator(promptSelector)).toHaveCount(0)
528
+ })
529
+ })
530
+ })