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,375 @@
|
|
|
1
|
+
import fs from "node:fs/promises"
|
|
2
|
+
import os from "node:os"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import { base64Decode } from "@reign-labs/util/encode"
|
|
5
|
+
import type { Page } from "@playwright/test"
|
|
6
|
+
|
|
7
|
+
import { test, expect } from "../fixtures"
|
|
8
|
+
|
|
9
|
+
test.describe.configure({ mode: "serial" })
|
|
10
|
+
import {
|
|
11
|
+
cleanupTestProject,
|
|
12
|
+
clickMenuItem,
|
|
13
|
+
confirmDialog,
|
|
14
|
+
openSidebar,
|
|
15
|
+
openWorkspaceMenu,
|
|
16
|
+
resolveSlug,
|
|
17
|
+
setWorkspacesEnabled,
|
|
18
|
+
slugFromUrl,
|
|
19
|
+
waitDir,
|
|
20
|
+
waitSlug,
|
|
21
|
+
} from "../actions"
|
|
22
|
+
import { dropdownMenuContentSelector, inlineInputSelector, workspaceItemSelector } from "../selectors"
|
|
23
|
+
import { createSdk, dirSlug } from "../utils"
|
|
24
|
+
|
|
25
|
+
async function setupWorkspaceTest(page: Page, project: { slug: string }) {
|
|
26
|
+
const rootSlug = project.slug
|
|
27
|
+
await openSidebar(page)
|
|
28
|
+
|
|
29
|
+
await setWorkspacesEnabled(page, rootSlug, true)
|
|
30
|
+
|
|
31
|
+
await page.getByRole("button", { name: "New workspace" }).first().click()
|
|
32
|
+
const next = await resolveSlug(await waitSlug(page, [rootSlug]))
|
|
33
|
+
await waitDir(page, next.directory)
|
|
34
|
+
|
|
35
|
+
await openSidebar(page)
|
|
36
|
+
|
|
37
|
+
await expect
|
|
38
|
+
.poll(
|
|
39
|
+
async () => {
|
|
40
|
+
const item = page.locator(workspaceItemSelector(next.slug)).first()
|
|
41
|
+
try {
|
|
42
|
+
await item.hover({ timeout: 500 })
|
|
43
|
+
return true
|
|
44
|
+
} catch {
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{ timeout: 60_000 },
|
|
49
|
+
)
|
|
50
|
+
.toBe(true)
|
|
51
|
+
|
|
52
|
+
return { rootSlug, slug: next.slug, directory: next.directory }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
test("can enable and disable workspaces from project menu", async ({ page, withProject }) => {
|
|
56
|
+
await page.setViewportSize({ width: 1400, height: 800 })
|
|
57
|
+
|
|
58
|
+
await withProject(async ({ slug }) => {
|
|
59
|
+
await openSidebar(page)
|
|
60
|
+
|
|
61
|
+
await expect(page.getByRole("button", { name: "New session" }).first()).toBeVisible()
|
|
62
|
+
await expect(page.getByRole("button", { name: "New workspace" })).toHaveCount(0)
|
|
63
|
+
|
|
64
|
+
await setWorkspacesEnabled(page, slug, true)
|
|
65
|
+
await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible()
|
|
66
|
+
await expect(page.locator(workspaceItemSelector(slug)).first()).toBeVisible()
|
|
67
|
+
|
|
68
|
+
await setWorkspacesEnabled(page, slug, false)
|
|
69
|
+
await expect(page.getByRole("button", { name: "New session" }).first()).toBeVisible()
|
|
70
|
+
await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0)
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test("can create a workspace", async ({ page, withProject }) => {
|
|
75
|
+
await page.setViewportSize({ width: 1400, height: 800 })
|
|
76
|
+
|
|
77
|
+
await withProject(async ({ slug }) => {
|
|
78
|
+
await openSidebar(page)
|
|
79
|
+
await setWorkspacesEnabled(page, slug, true)
|
|
80
|
+
|
|
81
|
+
await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible()
|
|
82
|
+
|
|
83
|
+
await page.getByRole("button", { name: "New workspace" }).first().click()
|
|
84
|
+
const next = await resolveSlug(await waitSlug(page, [slug]))
|
|
85
|
+
await waitDir(page, next.directory)
|
|
86
|
+
|
|
87
|
+
await openSidebar(page)
|
|
88
|
+
|
|
89
|
+
await expect
|
|
90
|
+
.poll(
|
|
91
|
+
async () => {
|
|
92
|
+
const item = page.locator(workspaceItemSelector(next.slug)).first()
|
|
93
|
+
try {
|
|
94
|
+
await item.hover({ timeout: 500 })
|
|
95
|
+
return true
|
|
96
|
+
} catch {
|
|
97
|
+
return false
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
{ timeout: 60_000 },
|
|
101
|
+
)
|
|
102
|
+
.toBe(true)
|
|
103
|
+
|
|
104
|
+
await expect(page.locator(workspaceItemSelector(next.slug)).first()).toBeVisible()
|
|
105
|
+
|
|
106
|
+
await cleanupTestProject(next.directory)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test("non-git projects keep workspace mode disabled", async ({ page, withProject }) => {
|
|
111
|
+
await page.setViewportSize({ width: 1400, height: 800 })
|
|
112
|
+
|
|
113
|
+
const nonGit = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-e2e-project-nongit-"))
|
|
114
|
+
const nonGitSlug = dirSlug(nonGit)
|
|
115
|
+
|
|
116
|
+
await fs.writeFile(path.join(nonGit, "README.md"), "# e2e nongit\n")
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
await withProject(async () => {
|
|
120
|
+
await page.goto(`/${nonGitSlug}/session`)
|
|
121
|
+
|
|
122
|
+
await expect.poll(() => slugFromUrl(page.url()), { timeout: 30_000 }).not.toBe("")
|
|
123
|
+
|
|
124
|
+
const activeDir = await resolveSlug(slugFromUrl(page.url())).then((item) => item.directory)
|
|
125
|
+
expect(path.basename(activeDir)).toContain("opencode-e2e-project-nongit-")
|
|
126
|
+
|
|
127
|
+
await openSidebar(page)
|
|
128
|
+
await expect(page.getByRole("button", { name: "New workspace" })).toHaveCount(0)
|
|
129
|
+
|
|
130
|
+
const trigger = page.locator('[data-action="project-menu"]').first()
|
|
131
|
+
const hasMenu = await trigger
|
|
132
|
+
.isVisible()
|
|
133
|
+
.then((x) => x)
|
|
134
|
+
.catch(() => false)
|
|
135
|
+
if (!hasMenu) return
|
|
136
|
+
|
|
137
|
+
await trigger.click({ force: true })
|
|
138
|
+
|
|
139
|
+
const menu = page.locator(dropdownMenuContentSelector).first()
|
|
140
|
+
await expect(menu).toBeVisible()
|
|
141
|
+
|
|
142
|
+
const toggle = menu.locator('[data-action="project-workspaces-toggle"]').first()
|
|
143
|
+
|
|
144
|
+
await expect(toggle).toBeVisible()
|
|
145
|
+
await expect(toggle).toBeDisabled()
|
|
146
|
+
await expect(menu.getByRole("menuitem", { name: "New workspace" })).toHaveCount(0)
|
|
147
|
+
})
|
|
148
|
+
} finally {
|
|
149
|
+
await cleanupTestProject(nonGit)
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test("can rename a workspace", async ({ page, withProject }) => {
|
|
154
|
+
await page.setViewportSize({ width: 1400, height: 800 })
|
|
155
|
+
|
|
156
|
+
await withProject(async (project) => {
|
|
157
|
+
const { slug } = await setupWorkspaceTest(page, project)
|
|
158
|
+
|
|
159
|
+
const rename = `e2e workspace ${Date.now()}`
|
|
160
|
+
const menu = await openWorkspaceMenu(page, slug)
|
|
161
|
+
await clickMenuItem(menu, /^Rename$/i, { force: true })
|
|
162
|
+
|
|
163
|
+
await expect(menu).toHaveCount(0)
|
|
164
|
+
|
|
165
|
+
const item = page.locator(workspaceItemSelector(slug)).first()
|
|
166
|
+
await expect(item).toBeVisible()
|
|
167
|
+
const input = item.locator(inlineInputSelector).first()
|
|
168
|
+
await expect(input).toBeVisible()
|
|
169
|
+
await input.fill(rename)
|
|
170
|
+
await input.press("Enter")
|
|
171
|
+
await expect(item).toContainText(rename)
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test("can reset a workspace", async ({ page, sdk, withProject }) => {
|
|
176
|
+
await page.setViewportSize({ width: 1400, height: 800 })
|
|
177
|
+
|
|
178
|
+
await withProject(async (project) => {
|
|
179
|
+
const { slug, directory: createdDir } = await setupWorkspaceTest(page, project)
|
|
180
|
+
|
|
181
|
+
const readme = path.join(createdDir, "README.md")
|
|
182
|
+
const extra = path.join(createdDir, `e2e_reset_${Date.now()}.txt`)
|
|
183
|
+
const original = await fs.readFile(readme, "utf8")
|
|
184
|
+
const dirty = `${original.trimEnd()}\n\nchange_${Date.now()}\n`
|
|
185
|
+
await fs.writeFile(readme, dirty, "utf8")
|
|
186
|
+
await fs.writeFile(extra, `created_${Date.now()}\n`, "utf8")
|
|
187
|
+
|
|
188
|
+
await expect
|
|
189
|
+
.poll(async () => {
|
|
190
|
+
return await fs
|
|
191
|
+
.stat(extra)
|
|
192
|
+
.then(() => true)
|
|
193
|
+
.catch(() => false)
|
|
194
|
+
})
|
|
195
|
+
.toBe(true)
|
|
196
|
+
|
|
197
|
+
await expect
|
|
198
|
+
.poll(async () => {
|
|
199
|
+
const files = await sdk.file
|
|
200
|
+
.status({ directory: createdDir })
|
|
201
|
+
.then((r) => r.data ?? [])
|
|
202
|
+
.catch(() => [])
|
|
203
|
+
return files.length
|
|
204
|
+
})
|
|
205
|
+
.toBeGreaterThan(0)
|
|
206
|
+
|
|
207
|
+
const menu = await openWorkspaceMenu(page, slug)
|
|
208
|
+
await clickMenuItem(menu, /^Reset$/i, { force: true })
|
|
209
|
+
await confirmDialog(page, /^Reset workspace$/i)
|
|
210
|
+
|
|
211
|
+
await expect
|
|
212
|
+
.poll(
|
|
213
|
+
async () => {
|
|
214
|
+
const files = await sdk.file
|
|
215
|
+
.status({ directory: createdDir })
|
|
216
|
+
.then((r) => r.data ?? [])
|
|
217
|
+
.catch(() => [])
|
|
218
|
+
return files.length
|
|
219
|
+
},
|
|
220
|
+
{ timeout: 60_000 },
|
|
221
|
+
)
|
|
222
|
+
.toBe(0)
|
|
223
|
+
|
|
224
|
+
await expect.poll(() => fs.readFile(readme, "utf8"), { timeout: 60_000 }).toBe(original)
|
|
225
|
+
|
|
226
|
+
await expect
|
|
227
|
+
.poll(async () => {
|
|
228
|
+
return await fs
|
|
229
|
+
.stat(extra)
|
|
230
|
+
.then(() => true)
|
|
231
|
+
.catch(() => false)
|
|
232
|
+
})
|
|
233
|
+
.toBe(false)
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
test("can delete a workspace", async ({ page, withProject }) => {
|
|
238
|
+
await page.setViewportSize({ width: 1400, height: 800 })
|
|
239
|
+
|
|
240
|
+
await withProject(async (project) => {
|
|
241
|
+
const sdk = createSdk(project.directory)
|
|
242
|
+
const { rootSlug, slug, directory } = await setupWorkspaceTest(page, project)
|
|
243
|
+
|
|
244
|
+
await expect
|
|
245
|
+
.poll(
|
|
246
|
+
async () => {
|
|
247
|
+
const worktrees = await sdk.worktree
|
|
248
|
+
.list()
|
|
249
|
+
.then((r) => r.data ?? [])
|
|
250
|
+
.catch(() => [] as string[])
|
|
251
|
+
return worktrees.includes(directory)
|
|
252
|
+
},
|
|
253
|
+
{ timeout: 30_000 },
|
|
254
|
+
)
|
|
255
|
+
.toBe(true)
|
|
256
|
+
|
|
257
|
+
const menu = await openWorkspaceMenu(page, slug)
|
|
258
|
+
await clickMenuItem(menu, /^Delete$/i, { force: true })
|
|
259
|
+
await confirmDialog(page, /^Delete workspace$/i)
|
|
260
|
+
|
|
261
|
+
await expect.poll(() => base64Decode(slugFromUrl(page.url()))).toBe(project.directory)
|
|
262
|
+
|
|
263
|
+
await expect
|
|
264
|
+
.poll(
|
|
265
|
+
async () => {
|
|
266
|
+
const worktrees = await sdk.worktree
|
|
267
|
+
.list()
|
|
268
|
+
.then((r) => r.data ?? [])
|
|
269
|
+
.catch(() => [] as string[])
|
|
270
|
+
return worktrees.includes(directory)
|
|
271
|
+
},
|
|
272
|
+
{ timeout: 60_000 },
|
|
273
|
+
)
|
|
274
|
+
.toBe(false)
|
|
275
|
+
|
|
276
|
+
await project.gotoSession()
|
|
277
|
+
|
|
278
|
+
await openSidebar(page)
|
|
279
|
+
await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0, { timeout: 60_000 })
|
|
280
|
+
await expect(page.locator(workspaceItemSelector(rootSlug)).first()).toBeVisible()
|
|
281
|
+
})
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
test("can reorder workspaces by drag and drop", async ({ page, withProject }) => {
|
|
285
|
+
await page.setViewportSize({ width: 1400, height: 800 })
|
|
286
|
+
await withProject(async ({ slug: rootSlug }) => {
|
|
287
|
+
const workspaces = [] as { directory: string; slug: string }[]
|
|
288
|
+
|
|
289
|
+
const listSlugs = async () => {
|
|
290
|
+
const nodes = page.locator('[data-component="sidebar-nav-desktop"] [data-component="workspace-item"]')
|
|
291
|
+
const slugs = await nodes.evaluateAll((els) => {
|
|
292
|
+
return els.map((el) => el.getAttribute("data-workspace") ?? "").filter((x) => x.length > 0)
|
|
293
|
+
})
|
|
294
|
+
return slugs
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const waitReady = async (slug: string) => {
|
|
298
|
+
await expect
|
|
299
|
+
.poll(
|
|
300
|
+
async () => {
|
|
301
|
+
const item = page.locator(workspaceItemSelector(slug)).first()
|
|
302
|
+
try {
|
|
303
|
+
await item.hover({ timeout: 500 })
|
|
304
|
+
return true
|
|
305
|
+
} catch {
|
|
306
|
+
return false
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
{ timeout: 60_000 },
|
|
310
|
+
)
|
|
311
|
+
.toBe(true)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const drag = async (from: string, to: string) => {
|
|
315
|
+
const src = page.locator(workspaceItemSelector(from)).first()
|
|
316
|
+
const dst = page.locator(workspaceItemSelector(to)).first()
|
|
317
|
+
|
|
318
|
+
const a = await src.boundingBox()
|
|
319
|
+
const b = await dst.boundingBox()
|
|
320
|
+
if (!a || !b) throw new Error("Failed to resolve workspace drag bounds")
|
|
321
|
+
|
|
322
|
+
await page.mouse.move(a.x + a.width / 2, a.y + a.height / 2)
|
|
323
|
+
await page.mouse.down()
|
|
324
|
+
await page.mouse.move(b.x + b.width / 2, b.y + b.height / 2, { steps: 12 })
|
|
325
|
+
await page.mouse.up()
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
await openSidebar(page)
|
|
330
|
+
|
|
331
|
+
await setWorkspacesEnabled(page, rootSlug, true)
|
|
332
|
+
|
|
333
|
+
for (const _ of [0, 1]) {
|
|
334
|
+
const prev = slugFromUrl(page.url())
|
|
335
|
+
await page.getByRole("button", { name: "New workspace" }).first().click()
|
|
336
|
+
const next = await resolveSlug(await waitSlug(page, [rootSlug, prev]))
|
|
337
|
+
await waitDir(page, next.directory)
|
|
338
|
+
workspaces.push(next)
|
|
339
|
+
|
|
340
|
+
await openSidebar(page)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (workspaces.length !== 2) throw new Error("Expected two created workspaces")
|
|
344
|
+
|
|
345
|
+
const a = workspaces[0].slug
|
|
346
|
+
const b = workspaces[1].slug
|
|
347
|
+
|
|
348
|
+
await waitReady(a)
|
|
349
|
+
await waitReady(b)
|
|
350
|
+
|
|
351
|
+
const list = async () => {
|
|
352
|
+
const slugs = await listSlugs()
|
|
353
|
+
return slugs.filter((s) => s !== rootSlug && (s === a || s === b)).slice(0, 2)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
await expect
|
|
357
|
+
.poll(async () => {
|
|
358
|
+
const slugs = await list()
|
|
359
|
+
return slugs.length === 2
|
|
360
|
+
})
|
|
361
|
+
.toBe(true)
|
|
362
|
+
|
|
363
|
+
const before = await list()
|
|
364
|
+
const from = before[1]
|
|
365
|
+
const to = before[0]
|
|
366
|
+
if (!from || !to) throw new Error("Failed to resolve initial workspace order")
|
|
367
|
+
|
|
368
|
+
await drag(from, to)
|
|
369
|
+
|
|
370
|
+
await expect.poll(async () => await list()).toEqual([from, to])
|
|
371
|
+
} finally {
|
|
372
|
+
await Promise.all(workspaces.map((w) => cleanupTestProject(w.directory)))
|
|
373
|
+
}
|
|
374
|
+
})
|
|
375
|
+
})
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { test, expect } from "../fixtures"
|
|
2
|
+
import type { Page } from "@playwright/test"
|
|
3
|
+
import { promptSelector } from "../selectors"
|
|
4
|
+
import { withSession } from "../actions"
|
|
5
|
+
|
|
6
|
+
function contextButton(page: Page) {
|
|
7
|
+
return page
|
|
8
|
+
.locator('[data-component="button"]')
|
|
9
|
+
.filter({ has: page.locator('[data-component="progress-circle"]').first() })
|
|
10
|
+
.first()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function seedContextSession(input: { sessionID: string; sdk: Parameters<typeof withSession>[0] }) {
|
|
14
|
+
await input.sdk.session.promptAsync({
|
|
15
|
+
sessionID: input.sessionID,
|
|
16
|
+
noReply: true,
|
|
17
|
+
parts: [
|
|
18
|
+
{
|
|
19
|
+
type: "text",
|
|
20
|
+
text: "seed context",
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
await expect
|
|
26
|
+
.poll(async () => {
|
|
27
|
+
const messages = await input.sdk.session
|
|
28
|
+
.messages({ sessionID: input.sessionID, limit: 1 })
|
|
29
|
+
.then((r) => r.data ?? [])
|
|
30
|
+
return messages.length
|
|
31
|
+
})
|
|
32
|
+
.toBeGreaterThan(0)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test("context panel can be opened from the prompt", async ({ page, sdk, gotoSession }) => {
|
|
36
|
+
const title = `e2e smoke context ${Date.now()}`
|
|
37
|
+
|
|
38
|
+
await withSession(sdk, title, async (session) => {
|
|
39
|
+
await seedContextSession({ sessionID: session.id, sdk })
|
|
40
|
+
|
|
41
|
+
await gotoSession(session.id)
|
|
42
|
+
|
|
43
|
+
const trigger = contextButton(page)
|
|
44
|
+
await expect(trigger).toBeVisible()
|
|
45
|
+
await trigger.click()
|
|
46
|
+
|
|
47
|
+
const tabs = page.locator('[data-component="tabs"][data-variant="normal"]')
|
|
48
|
+
await expect(tabs.getByRole("tab", { name: "Context" })).toBeVisible()
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test("context panel can be closed from the context tab close action", async ({ page, sdk, gotoSession }) => {
|
|
53
|
+
await withSession(sdk, `e2e context toggle ${Date.now()}`, async (session) => {
|
|
54
|
+
await seedContextSession({ sessionID: session.id, sdk })
|
|
55
|
+
await gotoSession(session.id)
|
|
56
|
+
|
|
57
|
+
await page.locator(promptSelector).click()
|
|
58
|
+
|
|
59
|
+
const trigger = contextButton(page)
|
|
60
|
+
await expect(trigger).toBeVisible()
|
|
61
|
+
await trigger.click()
|
|
62
|
+
|
|
63
|
+
const tabs = page.locator('[data-component="tabs"][data-variant="normal"]')
|
|
64
|
+
const context = tabs.getByRole("tab", { name: "Context" })
|
|
65
|
+
await expect(context).toBeVisible()
|
|
66
|
+
|
|
67
|
+
await page.getByRole("button", { name: "Close tab" }).first().click()
|
|
68
|
+
await expect(context).toHaveCount(0)
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test("context panel can open file picker from context actions", async ({ page, sdk, gotoSession }) => {
|
|
73
|
+
await withSession(sdk, `e2e context tabs ${Date.now()}`, async (session) => {
|
|
74
|
+
await seedContextSession({ sessionID: session.id, sdk })
|
|
75
|
+
await gotoSession(session.id)
|
|
76
|
+
|
|
77
|
+
await page.locator(promptSelector).click()
|
|
78
|
+
|
|
79
|
+
const trigger = contextButton(page)
|
|
80
|
+
await expect(trigger).toBeVisible()
|
|
81
|
+
await trigger.click()
|
|
82
|
+
|
|
83
|
+
await expect(page.getByRole("tab", { name: "Context" })).toBeVisible()
|
|
84
|
+
await page.getByRole("button", { name: "Open file" }).first().click()
|
|
85
|
+
|
|
86
|
+
const dialog = page
|
|
87
|
+
.getByRole("dialog")
|
|
88
|
+
.filter({ has: page.getByPlaceholder(/search files/i) })
|
|
89
|
+
.first()
|
|
90
|
+
await expect(dialog).toBeVisible()
|
|
91
|
+
|
|
92
|
+
await page.keyboard.press("Escape")
|
|
93
|
+
await expect(dialog).toHaveCount(0)
|
|
94
|
+
})
|
|
95
|
+
})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { test, expect } from "../fixtures"
|
|
2
|
+
import { promptSelector } from "../selectors"
|
|
3
|
+
import { cleanupSession, sessionIDFromUrl, withSession } from "../actions"
|
|
4
|
+
|
|
5
|
+
const text = (value: string | null) => (value ?? "").replace(/\u200B/g, "").trim()
|
|
6
|
+
|
|
7
|
+
// Regression test for Issue #12453: the synchronous POST /message endpoint holds
|
|
8
|
+
// the connection open while the agent works, causing "Failed to fetch" over
|
|
9
|
+
// VPN/Tailscale. The fix switches to POST /prompt_async which returns immediately.
|
|
10
|
+
test("prompt succeeds when sync message endpoint is unreachable", async ({ page, sdk, gotoSession }) => {
|
|
11
|
+
test.setTimeout(120_000)
|
|
12
|
+
|
|
13
|
+
// Simulate Tailscale/VPN killing the long-lived sync connection
|
|
14
|
+
await page.route("**/session/*/message", (route) => route.abort("connectionfailed"))
|
|
15
|
+
|
|
16
|
+
await gotoSession()
|
|
17
|
+
|
|
18
|
+
const token = `E2E_ASYNC_${Date.now()}`
|
|
19
|
+
await page.locator(promptSelector).click()
|
|
20
|
+
await page.keyboard.type(`Reply with exactly: ${token}`)
|
|
21
|
+
await page.keyboard.press("Enter")
|
|
22
|
+
|
|
23
|
+
await expect(page).toHaveURL(/\/session\/[^/?#]+/, { timeout: 30_000 })
|
|
24
|
+
const sessionID = sessionIDFromUrl(page.url())!
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// Agent response arrives via SSE despite sync endpoint being dead
|
|
28
|
+
await expect
|
|
29
|
+
.poll(
|
|
30
|
+
async () => {
|
|
31
|
+
const messages = await sdk.session.messages({ sessionID, limit: 50 }).then((r) => r.data ?? [])
|
|
32
|
+
return messages
|
|
33
|
+
.filter((m) => m.info.role === "assistant")
|
|
34
|
+
.flatMap((m) => m.parts)
|
|
35
|
+
.filter((p) => p.type === "text")
|
|
36
|
+
.map((p) => p.text)
|
|
37
|
+
.join("\n")
|
|
38
|
+
},
|
|
39
|
+
{ timeout: 90_000 },
|
|
40
|
+
)
|
|
41
|
+
.toContain(token)
|
|
42
|
+
} finally {
|
|
43
|
+
await cleanupSession({ sdk, sessionID })
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test("failed prompt send restores the composer input", async ({ page, sdk, gotoSession }) => {
|
|
48
|
+
await withSession(sdk, `e2e prompt failure ${Date.now()}`, async (session) => {
|
|
49
|
+
const prompt = page.locator(promptSelector)
|
|
50
|
+
const value = `restore ${Date.now()}`
|
|
51
|
+
|
|
52
|
+
await page.route(`**/session/${session.id}/prompt_async`, (route) =>
|
|
53
|
+
route.fulfill({
|
|
54
|
+
status: 500,
|
|
55
|
+
contentType: "application/json",
|
|
56
|
+
body: JSON.stringify({ message: "e2e prompt failure" }),
|
|
57
|
+
}),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
await gotoSession(session.id)
|
|
61
|
+
await prompt.click()
|
|
62
|
+
await page.keyboard.type(value)
|
|
63
|
+
await page.keyboard.press("Enter")
|
|
64
|
+
|
|
65
|
+
await expect.poll(async () => text(await prompt.textContent())).toBe(value)
|
|
66
|
+
await expect
|
|
67
|
+
.poll(
|
|
68
|
+
async () => {
|
|
69
|
+
const messages = await sdk.session.messages({ sessionID: session.id, limit: 50 }).then((r) => r.data ?? [])
|
|
70
|
+
return messages.length
|
|
71
|
+
},
|
|
72
|
+
{ timeout: 15_000 },
|
|
73
|
+
)
|
|
74
|
+
.toBe(0)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { test, expect } from "../fixtures"
|
|
2
|
+
import { promptSelector } from "../selectors"
|
|
3
|
+
|
|
4
|
+
test("dropping text/plain file: uri inserts a file pill", async ({ page, gotoSession }) => {
|
|
5
|
+
await gotoSession()
|
|
6
|
+
|
|
7
|
+
const prompt = page.locator(promptSelector)
|
|
8
|
+
await prompt.click()
|
|
9
|
+
|
|
10
|
+
const path = process.platform === "win32" ? "C:\\opencode-e2e-drop.txt" : "/tmp/opencode-e2e-drop.txt"
|
|
11
|
+
const dt = await page.evaluateHandle((text) => {
|
|
12
|
+
const dt = new DataTransfer()
|
|
13
|
+
dt.setData("text/plain", text)
|
|
14
|
+
return dt
|
|
15
|
+
}, `file:${path}`)
|
|
16
|
+
|
|
17
|
+
await page.dispatchEvent("body", "drop", { dataTransfer: dt })
|
|
18
|
+
|
|
19
|
+
const pill = page.locator(`${promptSelector} [data-type="file"]`).first()
|
|
20
|
+
await expect(pill).toBeVisible()
|
|
21
|
+
await expect(pill).toHaveAttribute("data-path", path)
|
|
22
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { test, expect } from "../fixtures"
|
|
2
|
+
import { promptSelector } from "../selectors"
|
|
3
|
+
|
|
4
|
+
test("dropping an image file adds an attachment", async ({ page, gotoSession }) => {
|
|
5
|
+
await gotoSession()
|
|
6
|
+
|
|
7
|
+
const prompt = page.locator(promptSelector)
|
|
8
|
+
await prompt.click()
|
|
9
|
+
|
|
10
|
+
const png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO3+4uQAAAAASUVORK5CYII="
|
|
11
|
+
const dt = await page.evaluateHandle((b64) => {
|
|
12
|
+
const dt = new DataTransfer()
|
|
13
|
+
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0))
|
|
14
|
+
const file = new File([bytes], "drop.png", { type: "image/png" })
|
|
15
|
+
dt.items.add(file)
|
|
16
|
+
return dt
|
|
17
|
+
}, png)
|
|
18
|
+
|
|
19
|
+
await page.dispatchEvent("body", "drop", { dataTransfer: dt })
|
|
20
|
+
|
|
21
|
+
const img = page.locator('img[alt="drop.png"]').first()
|
|
22
|
+
await expect(img).toBeVisible()
|
|
23
|
+
|
|
24
|
+
const remove = page.getByRole("button", { name: "Remove attachment" }).first()
|
|
25
|
+
await expect(remove).toBeVisible()
|
|
26
|
+
|
|
27
|
+
await img.hover()
|
|
28
|
+
await remove.click()
|
|
29
|
+
await expect(page.locator('img[alt="drop.png"]')).toHaveCount(0)
|
|
30
|
+
})
|