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.
- package/AGENTS.md +30 -0
- package/Dockerfile +21 -0
- package/README.md +51 -0
- package/bunfig.toml +3 -0
- package/create-effect-simplification-spec.md +515 -0
- package/e2e/AGENTS.md +226 -0
- package/e2e/actions.ts +1018 -0
- package/e2e/app/home.spec.ts +24 -0
- package/e2e/app/navigation.spec.ts +10 -0
- package/e2e/app/palette.spec.ts +20 -0
- package/e2e/app/server-default.spec.ts +58 -0
- package/e2e/app/session.spec.ts +16 -0
- package/e2e/app/titlebar-history.spec.ts +120 -0
- package/e2e/commands/input-focus.spec.ts +15 -0
- package/e2e/commands/panels.spec.ts +33 -0
- package/e2e/commands/tab-close.spec.ts +32 -0
- package/e2e/files/file-open.spec.ts +31 -0
- package/e2e/files/file-tree.spec.ts +56 -0
- package/e2e/files/file-viewer.spec.ts +156 -0
- package/e2e/fixtures.ts +154 -0
- package/e2e/models/model-picker.spec.ts +48 -0
- package/e2e/models/models-visibility.spec.ts +61 -0
- package/e2e/projects/project-edit.spec.ts +43 -0
- package/e2e/projects/projects-close.spec.ts +54 -0
- package/e2e/projects/projects-switch.spec.ts +116 -0
- package/e2e/projects/workspace-new-session.spec.ts +94 -0
- package/e2e/projects/workspaces.spec.ts +375 -0
- package/e2e/prompt/context.spec.ts +95 -0
- package/e2e/prompt/prompt-async.spec.ts +76 -0
- package/e2e/prompt/prompt-drop-file-uri.spec.ts +22 -0
- package/e2e/prompt/prompt-drop-file.spec.ts +30 -0
- package/e2e/prompt/prompt-history.spec.ts +184 -0
- package/e2e/prompt/prompt-mention.spec.ts +26 -0
- package/e2e/prompt/prompt-multiline.spec.ts +24 -0
- package/e2e/prompt/prompt-shell.spec.ts +62 -0
- package/e2e/prompt/prompt-slash-open.spec.ts +22 -0
- package/e2e/prompt/prompt-slash-share.spec.ts +64 -0
- package/e2e/prompt/prompt-slash-terminal.spec.ts +18 -0
- package/e2e/prompt/prompt.spec.ts +55 -0
- package/e2e/selectors.ts +75 -0
- package/e2e/session/session-child-navigation.spec.ts +37 -0
- package/e2e/session/session-composer-dock.spec.ts +530 -0
- package/e2e/session/session-model-persistence.spec.ts +359 -0
- package/e2e/session/session-review.spec.ts +426 -0
- package/e2e/session/session-undo-redo.spec.ts +233 -0
- package/e2e/session/session.spec.ts +174 -0
- package/e2e/settings/settings-keybinds.spec.ts +389 -0
- package/e2e/settings/settings-models.spec.ts +122 -0
- package/e2e/settings/settings-providers.spec.ts +136 -0
- package/e2e/settings/settings.spec.ts +519 -0
- package/e2e/sidebar/sidebar-popover-actions.spec.ts +118 -0
- package/e2e/sidebar/sidebar-session-links.spec.ts +30 -0
- package/e2e/sidebar/sidebar.spec.ts +40 -0
- package/e2e/status/status-popover.spec.ts +94 -0
- package/e2e/terminal/terminal-init.spec.ts +28 -0
- package/e2e/terminal/terminal-reconnect.spec.ts +46 -0
- package/e2e/terminal/terminal-tabs.spec.ts +168 -0
- package/e2e/terminal/terminal.spec.ts +18 -0
- package/e2e/thinking-level.spec.ts +25 -0
- package/e2e/tsconfig.json +9 -0
- package/e2e/utils.ts +63 -0
- package/happydom.ts +75 -0
- package/index.html +23 -0
- package/package.json +77 -0
- package/playwright.config.ts +45 -0
- package/public/_headers +17 -0
- package/public/oc-theme-preload.js +35 -0
- package/script/e2e-local.ts +180 -0
- package/src/addons/serialize.test.ts +319 -0
- package/src/addons/serialize.ts +634 -0
- package/src/app.tsx +308 -0
- package/src/components/debug-bar.tsx +443 -0
- package/src/components/dialog-connect-provider.tsx +617 -0
- package/src/components/dialog-custom-provider-form.ts +158 -0
- package/src/components/dialog-custom-provider.test.ts +80 -0
- package/src/components/dialog-custom-provider.tsx +329 -0
- package/src/components/dialog-edit-project.tsx +255 -0
- package/src/components/dialog-fork.tsx +108 -0
- package/src/components/dialog-manage-models.tsx +101 -0
- package/src/components/dialog-release-notes.tsx +144 -0
- package/src/components/dialog-select-directory.tsx +392 -0
- package/src/components/dialog-select-file.tsx +466 -0
- package/src/components/dialog-select-mcp.tsx +107 -0
- package/src/components/dialog-select-model-unpaid.tsx +137 -0
- package/src/components/dialog-select-model.tsx +220 -0
- package/src/components/dialog-select-provider.tsx +86 -0
- package/src/components/dialog-select-server.tsx +649 -0
- package/src/components/dialog-settings.tsx +73 -0
- package/src/components/file-tree.test.ts +78 -0
- package/src/components/file-tree.tsx +507 -0
- package/src/components/link.tsx +26 -0
- package/src/components/model-tooltip.tsx +91 -0
- package/src/components/prompt-input/attachments.test.ts +44 -0
- package/src/components/prompt-input/attachments.ts +201 -0
- package/src/components/prompt-input/build-request-parts.test.ts +312 -0
- package/src/components/prompt-input/build-request-parts.ts +175 -0
- package/src/components/prompt-input/context-items.tsx +88 -0
- package/src/components/prompt-input/drag-overlay.tsx +25 -0
- package/src/components/prompt-input/editor-dom.test.ts +99 -0
- package/src/components/prompt-input/editor-dom.ts +148 -0
- package/src/components/prompt-input/files.ts +66 -0
- package/src/components/prompt-input/history.test.ts +153 -0
- package/src/components/prompt-input/history.ts +256 -0
- package/src/components/prompt-input/image-attachments.tsx +58 -0
- package/src/components/prompt-input/paste.ts +24 -0
- package/src/components/prompt-input/placeholder.test.ts +48 -0
- package/src/components/prompt-input/placeholder.ts +15 -0
- package/src/components/prompt-input/slash-popover.tsx +141 -0
- package/src/components/prompt-input/submit.test.ts +346 -0
- package/src/components/prompt-input/submit.ts +579 -0
- package/src/components/prompt-input.tsx +1595 -0
- package/src/components/server/server-row.tsx +130 -0
- package/src/components/session/index.ts +5 -0
- package/src/components/session/session-context-breakdown.test.ts +61 -0
- package/src/components/session/session-context-breakdown.ts +132 -0
- package/src/components/session/session-context-format.ts +20 -0
- package/src/components/session/session-context-metrics.test.ts +101 -0
- package/src/components/session/session-context-metrics.ts +82 -0
- package/src/components/session/session-context-tab.tsx +339 -0
- package/src/components/session/session-header.tsx +486 -0
- package/src/components/session/session-new-view.tsx +91 -0
- package/src/components/session/session-sortable-tab.tsx +70 -0
- package/src/components/session/session-sortable-terminal-tab.tsx +193 -0
- package/src/components/session-context-usage.tsx +122 -0
- package/src/components/settings-general.tsx +585 -0
- package/src/components/settings-keybinds.tsx +453 -0
- package/src/components/settings-list.tsx +5 -0
- package/src/components/settings-models.tsx +137 -0
- package/src/components/settings-providers.tsx +251 -0
- package/src/components/status-popover.tsx +419 -0
- package/src/components/terminal.tsx +653 -0
- package/src/components/titlebar-history.test.ts +63 -0
- package/src/components/titlebar-history.ts +57 -0
- package/src/components/titlebar.tsx +312 -0
- package/src/constants/file-picker.ts +89 -0
- package/src/context/command-keybind.test.ts +69 -0
- package/src/context/command.test.ts +25 -0
- package/src/context/command.tsx +437 -0
- package/src/context/comments.test.ts +186 -0
- package/src/context/comments.tsx +243 -0
- package/src/context/file/content-cache.ts +88 -0
- package/src/context/file/path.test.ts +360 -0
- package/src/context/file/path.ts +151 -0
- package/src/context/file/tree-store.ts +170 -0
- package/src/context/file/types.ts +41 -0
- package/src/context/file/view-cache.ts +146 -0
- package/src/context/file/watcher.test.ts +149 -0
- package/src/context/file/watcher.ts +53 -0
- package/src/context/file-content-eviction-accounting.test.ts +65 -0
- package/src/context/file.tsx +280 -0
- package/src/context/global-sdk.tsx +232 -0
- package/src/context/global-sync/bootstrap.ts +206 -0
- package/src/context/global-sync/child-store.test.ts +38 -0
- package/src/context/global-sync/child-store.ts +281 -0
- package/src/context/global-sync/event-reducer.test.ts +552 -0
- package/src/context/global-sync/event-reducer.ts +359 -0
- package/src/context/global-sync/eviction.ts +28 -0
- package/src/context/global-sync/queue.ts +83 -0
- package/src/context/global-sync/session-cache.test.ts +102 -0
- package/src/context/global-sync/session-cache.ts +62 -0
- package/src/context/global-sync/session-load.ts +25 -0
- package/src/context/global-sync/session-prefetch.test.ts +96 -0
- package/src/context/global-sync/session-prefetch.ts +100 -0
- package/src/context/global-sync/session-trim.test.ts +59 -0
- package/src/context/global-sync/session-trim.ts +56 -0
- package/src/context/global-sync/types.ts +133 -0
- package/src/context/global-sync/utils.ts +25 -0
- package/src/context/global-sync.test.ts +122 -0
- package/src/context/global-sync.tsx +408 -0
- package/src/context/highlights.tsx +233 -0
- package/src/context/language.tsx +248 -0
- package/src/context/layout-scroll.test.ts +64 -0
- package/src/context/layout-scroll.ts +126 -0
- package/src/context/layout.test.ts +69 -0
- package/src/context/layout.tsx +937 -0
- package/src/context/local.tsx +422 -0
- package/src/context/model-variant.test.ts +86 -0
- package/src/context/model-variant.ts +52 -0
- package/src/context/models.tsx +163 -0
- package/src/context/notification.tsx +373 -0
- package/src/context/permission-auto-respond.test.ts +102 -0
- package/src/context/permission-auto-respond.ts +51 -0
- package/src/context/permission.tsx +277 -0
- package/src/context/platform.tsx +99 -0
- package/src/context/prompt.tsx +297 -0
- package/src/context/sdk.tsx +49 -0
- package/src/context/server.tsx +295 -0
- package/src/context/settings.tsx +241 -0
- package/src/context/sync-optimistic.test.ts +123 -0
- package/src/context/sync.tsx +618 -0
- package/src/context/terminal-title.ts +51 -0
- package/src/context/terminal.test.ts +82 -0
- package/src/context/terminal.tsx +437 -0
- package/src/entry.tsx +144 -0
- package/src/env.d.ts +18 -0
- package/src/hooks/use-providers.ts +44 -0
- package/src/i18n/ar.ts +855 -0
- package/src/i18n/br.ts +867 -0
- package/src/i18n/bs.ts +943 -0
- package/src/i18n/da.ts +937 -0
- package/src/i18n/de.ts +879 -0
- package/src/i18n/en.ts +948 -0
- package/src/i18n/es.ts +950 -0
- package/src/i18n/fr.ts +878 -0
- package/src/i18n/ja.ts +861 -0
- package/src/i18n/ko.ts +860 -0
- package/src/i18n/no.ts +944 -0
- package/src/i18n/parity.test.ts +32 -0
- package/src/i18n/pl.ts +865 -0
- package/src/i18n/ru.ts +946 -0
- package/src/i18n/th.ts +933 -0
- package/src/i18n/tr.ts +952 -0
- package/src/i18n/zh.ts +930 -0
- package/src/i18n/zht.ts +925 -0
- package/src/index.css +29 -0
- package/src/index.ts +6 -0
- package/src/pages/directory-layout.tsx +88 -0
- package/src/pages/error.tsx +327 -0
- package/src/pages/home.tsx +131 -0
- package/src/pages/layout/deep-links.ts +50 -0
- package/src/pages/layout/helpers.test.ts +211 -0
- package/src/pages/layout/helpers.ts +98 -0
- package/src/pages/layout/inline-editor.tsx +126 -0
- package/src/pages/layout/sidebar-items.tsx +437 -0
- package/src/pages/layout/sidebar-project.tsx +384 -0
- package/src/pages/layout/sidebar-shell.tsx +125 -0
- package/src/pages/layout/sidebar-workspace.tsx +504 -0
- package/src/pages/layout.tsx +2509 -0
- package/src/pages/session/composer/index.ts +2 -0
- package/src/pages/session/composer/session-composer-region.tsx +255 -0
- package/src/pages/session/composer/session-composer-state.test.ts +128 -0
- package/src/pages/session/composer/session-composer-state.ts +249 -0
- package/src/pages/session/composer/session-followup-dock.tsx +109 -0
- package/src/pages/session/composer/session-permission-dock.tsx +74 -0
- package/src/pages/session/composer/session-question-dock.tsx +449 -0
- package/src/pages/session/composer/session-request-tree.ts +52 -0
- package/src/pages/session/composer/session-revert-dock.tsx +99 -0
- package/src/pages/session/composer/session-todo-dock.tsx +330 -0
- package/src/pages/session/file-tab-scroll.test.ts +40 -0
- package/src/pages/session/file-tab-scroll.ts +67 -0
- package/src/pages/session/file-tabs.tsx +456 -0
- package/src/pages/session/handoff.ts +36 -0
- package/src/pages/session/helpers.test.ts +181 -0
- package/src/pages/session/helpers.ts +198 -0
- package/src/pages/session/message-gesture.test.ts +62 -0
- package/src/pages/session/message-gesture.ts +21 -0
- package/src/pages/session/message-id-from-hash.ts +6 -0
- package/src/pages/session/message-timeline.tsx +1013 -0
- package/src/pages/session/review-tab.tsx +170 -0
- package/src/pages/session/session-layout.ts +20 -0
- package/src/pages/session/session-model-helpers.test.ts +51 -0
- package/src/pages/session/session-model-helpers.ts +16 -0
- package/src/pages/session/session-side-panel.tsx +453 -0
- package/src/pages/session/terminal-label.ts +16 -0
- package/src/pages/session/terminal-panel.test.ts +25 -0
- package/src/pages/session/terminal-panel.tsx +326 -0
- package/src/pages/session/use-session-commands.tsx +495 -0
- package/src/pages/session/use-session-hash-scroll.test.ts +16 -0
- package/src/pages/session/use-session-hash-scroll.ts +197 -0
- package/src/pages/session.tsx +1841 -0
- package/src/sst-env.d.ts +12 -0
- package/src/testing/model-selection.ts +80 -0
- package/src/testing/prompt.ts +56 -0
- package/src/testing/session-composer.ts +84 -0
- package/src/testing/terminal.ts +118 -0
- package/src/theme-preload.test.ts +46 -0
- package/src/utils/agent.ts +23 -0
- package/src/utils/aim.ts +138 -0
- package/src/utils/base64.ts +10 -0
- package/src/utils/comment-note.ts +88 -0
- package/src/utils/id.ts +99 -0
- package/src/utils/notification-click.test.ts +27 -0
- package/src/utils/notification-click.ts +13 -0
- package/src/utils/persist.test.ts +115 -0
- package/src/utils/persist.ts +476 -0
- package/src/utils/prompt.test.ts +44 -0
- package/src/utils/prompt.ts +203 -0
- package/src/utils/runtime-adapters.test.ts +62 -0
- package/src/utils/runtime-adapters.ts +39 -0
- package/src/utils/same.ts +6 -0
- package/src/utils/scoped-cache.test.ts +69 -0
- package/src/utils/scoped-cache.ts +104 -0
- package/src/utils/server-errors.test.ts +131 -0
- package/src/utils/server-errors.ts +80 -0
- package/src/utils/server-health.test.ts +123 -0
- package/src/utils/server-health.ts +91 -0
- package/src/utils/server.ts +22 -0
- package/src/utils/solid-dnd.tsx +49 -0
- package/src/utils/sound.ts +117 -0
- package/src/utils/terminal-writer.test.ts +64 -0
- package/src/utils/terminal-writer.ts +65 -0
- package/src/utils/time.ts +22 -0
- package/src/utils/uuid.test.ts +78 -0
- package/src/utils/uuid.ts +12 -0
- package/src/utils/worktree.test.ts +46 -0
- package/src/utils/worktree.ts +73 -0
- package/sst-env.d.ts +10 -0
- package/tsconfig.json +26 -0
- package/vite.config.ts +15 -0
- 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
|
+
})
|