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
package/e2e/fixtures.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { test as base, expect, type Page } from "@playwright/test"
|
|
2
|
+
import type { E2EWindow } from "../src/testing/terminal"
|
|
3
|
+
import {
|
|
4
|
+
healthPhase,
|
|
5
|
+
cleanupSession,
|
|
6
|
+
cleanupTestProject,
|
|
7
|
+
createTestProject,
|
|
8
|
+
setHealthPhase,
|
|
9
|
+
seedProjects,
|
|
10
|
+
sessionIDFromUrl,
|
|
11
|
+
waitSlug,
|
|
12
|
+
waitSession,
|
|
13
|
+
} from "./actions"
|
|
14
|
+
import { createSdk, dirSlug, getWorktree, sessionPath } from "./utils"
|
|
15
|
+
|
|
16
|
+
export const settingsKey = "settings.v3"
|
|
17
|
+
|
|
18
|
+
type TestFixtures = {
|
|
19
|
+
sdk: ReturnType<typeof createSdk>
|
|
20
|
+
gotoSession: (sessionID?: string) => Promise<void>
|
|
21
|
+
withProject: <T>(
|
|
22
|
+
callback: (project: {
|
|
23
|
+
directory: string
|
|
24
|
+
slug: string
|
|
25
|
+
gotoSession: (sessionID?: string) => Promise<void>
|
|
26
|
+
trackSession: (sessionID: string, directory?: string) => void
|
|
27
|
+
trackDirectory: (directory: string) => void
|
|
28
|
+
}) => Promise<T>,
|
|
29
|
+
options?: { extra?: string[] },
|
|
30
|
+
) => Promise<T>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type WorkerFixtures = {
|
|
34
|
+
directory: string
|
|
35
|
+
slug: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const test = base.extend<TestFixtures, WorkerFixtures>({
|
|
39
|
+
page: async ({ page }, use) => {
|
|
40
|
+
let boundary: string | undefined
|
|
41
|
+
setHealthPhase(page, "test")
|
|
42
|
+
const consoleHandler = (msg: { text(): string }) => {
|
|
43
|
+
const text = msg.text()
|
|
44
|
+
if (!text.includes("[e2e:error-boundary]")) return
|
|
45
|
+
if (healthPhase(page) === "cleanup") {
|
|
46
|
+
console.warn(`[e2e:error-boundary][cleanup-warning]\n${text}`)
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
boundary ||= text
|
|
50
|
+
console.log(text)
|
|
51
|
+
}
|
|
52
|
+
const pageErrorHandler = (err: Error) => {
|
|
53
|
+
console.log(`[e2e:pageerror] ${err.stack || err.message}`)
|
|
54
|
+
}
|
|
55
|
+
page.on("console", consoleHandler)
|
|
56
|
+
page.on("pageerror", pageErrorHandler)
|
|
57
|
+
await use(page)
|
|
58
|
+
page.off("console", consoleHandler)
|
|
59
|
+
page.off("pageerror", pageErrorHandler)
|
|
60
|
+
if (boundary) throw new Error(boundary)
|
|
61
|
+
},
|
|
62
|
+
directory: [
|
|
63
|
+
async ({}, use) => {
|
|
64
|
+
const directory = await getWorktree()
|
|
65
|
+
await use(directory)
|
|
66
|
+
},
|
|
67
|
+
{ scope: "worker" },
|
|
68
|
+
],
|
|
69
|
+
slug: [
|
|
70
|
+
async ({ directory }, use) => {
|
|
71
|
+
await use(dirSlug(directory))
|
|
72
|
+
},
|
|
73
|
+
{ scope: "worker" },
|
|
74
|
+
],
|
|
75
|
+
sdk: async ({ directory }, use) => {
|
|
76
|
+
await use(createSdk(directory))
|
|
77
|
+
},
|
|
78
|
+
gotoSession: async ({ page, directory }, use) => {
|
|
79
|
+
await seedStorage(page, { directory })
|
|
80
|
+
|
|
81
|
+
const gotoSession = async (sessionID?: string) => {
|
|
82
|
+
await page.goto(sessionPath(directory, sessionID))
|
|
83
|
+
await waitSession(page, { directory, sessionID })
|
|
84
|
+
}
|
|
85
|
+
await use(gotoSession)
|
|
86
|
+
},
|
|
87
|
+
withProject: async ({ page }, use) => {
|
|
88
|
+
await use(async (callback, options) => {
|
|
89
|
+
const root = await createTestProject()
|
|
90
|
+
const sessions = new Map<string, string>()
|
|
91
|
+
const dirs = new Set<string>()
|
|
92
|
+
await seedStorage(page, { directory: root, extra: options?.extra })
|
|
93
|
+
|
|
94
|
+
const gotoSession = async (sessionID?: string) => {
|
|
95
|
+
await page.goto(sessionPath(root, sessionID))
|
|
96
|
+
await waitSession(page, { directory: root, sessionID })
|
|
97
|
+
const current = sessionIDFromUrl(page.url())
|
|
98
|
+
if (current) trackSession(current)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const trackSession = (sessionID: string, directory?: string) => {
|
|
102
|
+
sessions.set(sessionID, directory ?? root)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const trackDirectory = (directory: string) => {
|
|
106
|
+
if (directory !== root) dirs.add(directory)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
await gotoSession()
|
|
111
|
+
const slug = await waitSlug(page)
|
|
112
|
+
return await callback({ directory: root, slug, gotoSession, trackSession, trackDirectory })
|
|
113
|
+
} finally {
|
|
114
|
+
setHealthPhase(page, "cleanup")
|
|
115
|
+
await Promise.allSettled(
|
|
116
|
+
Array.from(sessions, ([sessionID, directory]) => cleanupSession({ sessionID, directory })),
|
|
117
|
+
)
|
|
118
|
+
await Promise.allSettled(Array.from(dirs, (directory) => cleanupTestProject(directory)))
|
|
119
|
+
await cleanupTestProject(root)
|
|
120
|
+
setHealthPhase(page, "test")
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
async function seedStorage(page: Page, input: { directory: string; extra?: string[] }) {
|
|
127
|
+
await seedProjects(page, input)
|
|
128
|
+
await page.addInitScript(() => {
|
|
129
|
+
const win = window as E2EWindow
|
|
130
|
+
win.__opencode_e2e = {
|
|
131
|
+
...win.__opencode_e2e,
|
|
132
|
+
model: {
|
|
133
|
+
enabled: true,
|
|
134
|
+
},
|
|
135
|
+
prompt: {
|
|
136
|
+
enabled: true,
|
|
137
|
+
},
|
|
138
|
+
terminal: {
|
|
139
|
+
enabled: true,
|
|
140
|
+
terminals: {},
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
localStorage.setItem(
|
|
144
|
+
"opencode.global.dat:model",
|
|
145
|
+
JSON.stringify({
|
|
146
|
+
recent: [{ providerID: "opencode", modelID: "big-pickle" }],
|
|
147
|
+
user: [],
|
|
148
|
+
variant: {},
|
|
149
|
+
}),
|
|
150
|
+
)
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export { expect }
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { test, expect } from "../fixtures"
|
|
2
|
+
import { promptSelector } from "../selectors"
|
|
3
|
+
import { clickListItem } from "../actions"
|
|
4
|
+
|
|
5
|
+
test("smoke model selection updates prompt footer", async ({ page, gotoSession }) => {
|
|
6
|
+
await gotoSession()
|
|
7
|
+
|
|
8
|
+
await page.locator(promptSelector).click()
|
|
9
|
+
await page.keyboard.type("/model")
|
|
10
|
+
|
|
11
|
+
const command = page.locator('[data-slash-id="model.choose"]')
|
|
12
|
+
await expect(command).toBeVisible()
|
|
13
|
+
await command.hover()
|
|
14
|
+
|
|
15
|
+
await page.keyboard.press("Enter")
|
|
16
|
+
|
|
17
|
+
const dialog = page.getByRole("dialog")
|
|
18
|
+
await expect(dialog).toBeVisible()
|
|
19
|
+
|
|
20
|
+
const input = dialog.getByRole("textbox").first()
|
|
21
|
+
|
|
22
|
+
const selected = dialog.locator('[data-slot="list-item"][data-selected="true"]').first()
|
|
23
|
+
await expect(selected).toBeVisible()
|
|
24
|
+
|
|
25
|
+
const other = dialog.locator('[data-slot="list-item"]:not([data-selected="true"])').first()
|
|
26
|
+
const target = (await other.count()) > 0 ? other : selected
|
|
27
|
+
|
|
28
|
+
const key = await target.getAttribute("data-key")
|
|
29
|
+
if (!key) throw new Error("Failed to resolve model key from list item")
|
|
30
|
+
|
|
31
|
+
const model = key.split(":").slice(1).join(":")
|
|
32
|
+
|
|
33
|
+
await input.fill(model)
|
|
34
|
+
|
|
35
|
+
await clickListItem(dialog, { key })
|
|
36
|
+
|
|
37
|
+
await expect(dialog).toHaveCount(0)
|
|
38
|
+
|
|
39
|
+
await page.locator(promptSelector).click()
|
|
40
|
+
await page.keyboard.type("/model")
|
|
41
|
+
await expect(command).toBeVisible()
|
|
42
|
+
await command.hover()
|
|
43
|
+
await page.keyboard.press("Enter")
|
|
44
|
+
|
|
45
|
+
const dialogAgain = page.getByRole("dialog")
|
|
46
|
+
await expect(dialogAgain).toBeVisible()
|
|
47
|
+
await expect(dialogAgain.locator(`[data-slot="list-item"][data-key="${key}"][data-selected="true"]`)).toBeVisible()
|
|
48
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { test, expect } from "../fixtures"
|
|
2
|
+
import { promptSelector } from "../selectors"
|
|
3
|
+
import { closeDialog, openSettings, clickListItem } from "../actions"
|
|
4
|
+
|
|
5
|
+
test("hiding a model removes it from the model picker", async ({ page, gotoSession }) => {
|
|
6
|
+
await gotoSession()
|
|
7
|
+
|
|
8
|
+
await page.locator(promptSelector).click()
|
|
9
|
+
await page.keyboard.type("/model")
|
|
10
|
+
|
|
11
|
+
const command = page.locator('[data-slash-id="model.choose"]')
|
|
12
|
+
await expect(command).toBeVisible()
|
|
13
|
+
await command.hover()
|
|
14
|
+
await page.keyboard.press("Enter")
|
|
15
|
+
|
|
16
|
+
const picker = page.getByRole("dialog")
|
|
17
|
+
await expect(picker).toBeVisible()
|
|
18
|
+
|
|
19
|
+
const target = picker.locator('[data-slot="list-item"]').first()
|
|
20
|
+
await expect(target).toBeVisible()
|
|
21
|
+
|
|
22
|
+
const key = await target.getAttribute("data-key")
|
|
23
|
+
if (!key) throw new Error("Failed to resolve model key from list item")
|
|
24
|
+
|
|
25
|
+
const name = (await target.locator("span").first().innerText()).trim()
|
|
26
|
+
if (!name) throw new Error("Failed to resolve model name from list item")
|
|
27
|
+
|
|
28
|
+
await page.keyboard.press("Escape")
|
|
29
|
+
await expect(picker).toHaveCount(0)
|
|
30
|
+
|
|
31
|
+
const settings = await openSettings(page)
|
|
32
|
+
|
|
33
|
+
await settings.getByRole("tab", { name: "Models" }).click()
|
|
34
|
+
const search = settings.getByPlaceholder("Search models")
|
|
35
|
+
await expect(search).toBeVisible()
|
|
36
|
+
await search.fill(name)
|
|
37
|
+
|
|
38
|
+
const toggle = settings.locator('[data-component="switch"]').filter({ hasText: name }).first()
|
|
39
|
+
const input = toggle.locator('[data-slot="switch-input"]')
|
|
40
|
+
await expect(toggle).toBeVisible()
|
|
41
|
+
await expect(input).toHaveAttribute("aria-checked", "true")
|
|
42
|
+
await toggle.locator('[data-slot="switch-control"]').click()
|
|
43
|
+
await expect(input).toHaveAttribute("aria-checked", "false")
|
|
44
|
+
|
|
45
|
+
await closeDialog(page, settings)
|
|
46
|
+
|
|
47
|
+
await page.locator(promptSelector).click()
|
|
48
|
+
await page.keyboard.type("/model")
|
|
49
|
+
await expect(command).toBeVisible()
|
|
50
|
+
await command.hover()
|
|
51
|
+
await page.keyboard.press("Enter")
|
|
52
|
+
|
|
53
|
+
const pickerAgain = page.getByRole("dialog")
|
|
54
|
+
await expect(pickerAgain).toBeVisible()
|
|
55
|
+
await expect(pickerAgain.locator('[data-slot="list-item"]').first()).toBeVisible()
|
|
56
|
+
|
|
57
|
+
await expect(pickerAgain.locator(`[data-slot="list-item"][data-key="${key}"]`)).toHaveCount(0)
|
|
58
|
+
|
|
59
|
+
await page.keyboard.press("Escape")
|
|
60
|
+
await expect(pickerAgain).toHaveCount(0)
|
|
61
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { test, expect } from "../fixtures"
|
|
2
|
+
import { clickMenuItem, openProjectMenu, openSidebar } from "../actions"
|
|
3
|
+
|
|
4
|
+
test("dialog edit project updates name and startup script", async ({ page, withProject }) => {
|
|
5
|
+
await page.setViewportSize({ width: 1400, height: 800 })
|
|
6
|
+
|
|
7
|
+
await withProject(async ({ slug }) => {
|
|
8
|
+
await openSidebar(page)
|
|
9
|
+
|
|
10
|
+
const open = async () => {
|
|
11
|
+
const menu = await openProjectMenu(page, slug)
|
|
12
|
+
await clickMenuItem(menu, /^Edit$/i, { force: true })
|
|
13
|
+
|
|
14
|
+
const dialog = page.getByRole("dialog")
|
|
15
|
+
await expect(dialog).toBeVisible()
|
|
16
|
+
await expect(dialog.getByRole("heading", { level: 2 })).toHaveText("Edit project")
|
|
17
|
+
return dialog
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const name = `e2e project ${Date.now()}`
|
|
21
|
+
const startup = `echo e2e_${Date.now()}`
|
|
22
|
+
|
|
23
|
+
const dialog = await open()
|
|
24
|
+
|
|
25
|
+
const nameInput = dialog.getByLabel("Name")
|
|
26
|
+
await nameInput.fill(name)
|
|
27
|
+
|
|
28
|
+
const startupInput = dialog.getByLabel("Workspace startup script")
|
|
29
|
+
await startupInput.fill(startup)
|
|
30
|
+
|
|
31
|
+
await dialog.getByRole("button", { name: "Save" }).click()
|
|
32
|
+
await expect(dialog).toHaveCount(0)
|
|
33
|
+
|
|
34
|
+
const header = page.locator(".group\\/project").first()
|
|
35
|
+
await expect(header).toContainText(name)
|
|
36
|
+
|
|
37
|
+
const reopened = await open()
|
|
38
|
+
await expect(reopened.getByLabel("Name")).toHaveValue(name)
|
|
39
|
+
await expect(reopened.getByLabel("Workspace startup script")).toHaveValue(startup)
|
|
40
|
+
await reopened.getByRole("button", { name: "Cancel" }).click()
|
|
41
|
+
await expect(reopened).toHaveCount(0)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { test, expect } from "../fixtures"
|
|
2
|
+
import { createTestProject, cleanupTestProject, openSidebar, clickMenuItem, openProjectMenu } from "../actions"
|
|
3
|
+
import { projectSwitchSelector } from "../selectors"
|
|
4
|
+
import { dirSlug } from "../utils"
|
|
5
|
+
|
|
6
|
+
test("closing active project navigates to another open project", async ({ page, withProject }) => {
|
|
7
|
+
await page.setViewportSize({ width: 1400, height: 800 })
|
|
8
|
+
|
|
9
|
+
const other = await createTestProject()
|
|
10
|
+
const otherSlug = dirSlug(other)
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
await withProject(
|
|
14
|
+
async ({ slug }) => {
|
|
15
|
+
await openSidebar(page)
|
|
16
|
+
|
|
17
|
+
const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
|
|
18
|
+
await expect(otherButton).toBeVisible()
|
|
19
|
+
await otherButton.click()
|
|
20
|
+
|
|
21
|
+
await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`))
|
|
22
|
+
|
|
23
|
+
const menu = await openProjectMenu(page, otherSlug)
|
|
24
|
+
|
|
25
|
+
await clickMenuItem(menu, /^Close$/i, { force: true })
|
|
26
|
+
|
|
27
|
+
await expect
|
|
28
|
+
.poll(
|
|
29
|
+
() => {
|
|
30
|
+
const pathname = new URL(page.url()).pathname
|
|
31
|
+
if (new RegExp(`^/${slug}/session(?:/[^/]+)?/?$`).test(pathname)) return "project"
|
|
32
|
+
if (pathname === "/") return "home"
|
|
33
|
+
return ""
|
|
34
|
+
},
|
|
35
|
+
{ timeout: 15_000 },
|
|
36
|
+
)
|
|
37
|
+
.toMatch(/^(project|home)$/)
|
|
38
|
+
|
|
39
|
+
await expect(page).not.toHaveURL(new RegExp(`/${otherSlug}/session(?:[/?#]|$)`))
|
|
40
|
+
await expect
|
|
41
|
+
.poll(
|
|
42
|
+
async () => {
|
|
43
|
+
return await page.locator(projectSwitchSelector(otherSlug)).count()
|
|
44
|
+
},
|
|
45
|
+
{ timeout: 15_000 },
|
|
46
|
+
)
|
|
47
|
+
.toBe(0)
|
|
48
|
+
},
|
|
49
|
+
{ extra: [other] },
|
|
50
|
+
)
|
|
51
|
+
} finally {
|
|
52
|
+
await cleanupTestProject(other)
|
|
53
|
+
}
|
|
54
|
+
})
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { base64Decode } from "@reign-labs/util/encode"
|
|
2
|
+
import { test, expect } from "../fixtures"
|
|
3
|
+
import {
|
|
4
|
+
defocus,
|
|
5
|
+
createTestProject,
|
|
6
|
+
cleanupTestProject,
|
|
7
|
+
openSidebar,
|
|
8
|
+
sessionIDFromUrl,
|
|
9
|
+
setWorkspacesEnabled,
|
|
10
|
+
waitSession,
|
|
11
|
+
waitSessionSaved,
|
|
12
|
+
waitSlug,
|
|
13
|
+
} from "../actions"
|
|
14
|
+
import { projectSwitchSelector, promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
|
|
15
|
+
import { dirSlug, resolveDirectory } from "../utils"
|
|
16
|
+
|
|
17
|
+
test("can switch between projects from sidebar", async ({ page, withProject }) => {
|
|
18
|
+
await page.setViewportSize({ width: 1400, height: 800 })
|
|
19
|
+
|
|
20
|
+
const other = await createTestProject()
|
|
21
|
+
const otherSlug = dirSlug(other)
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
await withProject(
|
|
25
|
+
async ({ directory }) => {
|
|
26
|
+
await defocus(page)
|
|
27
|
+
|
|
28
|
+
const currentSlug = dirSlug(directory)
|
|
29
|
+
const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
|
|
30
|
+
await expect(otherButton).toBeVisible()
|
|
31
|
+
await otherButton.click()
|
|
32
|
+
|
|
33
|
+
await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`))
|
|
34
|
+
|
|
35
|
+
const currentButton = page.locator(projectSwitchSelector(currentSlug)).first()
|
|
36
|
+
await expect(currentButton).toBeVisible()
|
|
37
|
+
await currentButton.click()
|
|
38
|
+
|
|
39
|
+
await expect(page).toHaveURL(new RegExp(`/${currentSlug}/session`))
|
|
40
|
+
},
|
|
41
|
+
{ extra: [other] },
|
|
42
|
+
)
|
|
43
|
+
} finally {
|
|
44
|
+
await cleanupTestProject(other)
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test("switching back to a project opens the latest workspace session", async ({ page, withProject }) => {
|
|
49
|
+
await page.setViewportSize({ width: 1400, height: 800 })
|
|
50
|
+
|
|
51
|
+
const other = await createTestProject()
|
|
52
|
+
const otherSlug = dirSlug(other)
|
|
53
|
+
try {
|
|
54
|
+
await withProject(
|
|
55
|
+
async ({ directory, slug, trackSession, trackDirectory }) => {
|
|
56
|
+
await defocus(page)
|
|
57
|
+
await setWorkspacesEnabled(page, slug, true)
|
|
58
|
+
await openSidebar(page)
|
|
59
|
+
await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible()
|
|
60
|
+
|
|
61
|
+
await page.getByRole("button", { name: "New workspace" }).first().click()
|
|
62
|
+
|
|
63
|
+
const raw = await waitSlug(page, [slug])
|
|
64
|
+
const dir = base64Decode(raw)
|
|
65
|
+
if (!dir) throw new Error(`Failed to decode workspace slug: ${raw}`)
|
|
66
|
+
const space = await resolveDirectory(dir)
|
|
67
|
+
const next = dirSlug(space)
|
|
68
|
+
trackDirectory(space)
|
|
69
|
+
await openSidebar(page)
|
|
70
|
+
|
|
71
|
+
const item = page.locator(`${workspaceItemSelector(next)}, ${workspaceItemSelector(raw)}`).first()
|
|
72
|
+
await expect(item).toBeVisible()
|
|
73
|
+
await item.hover()
|
|
74
|
+
|
|
75
|
+
const btn = page.locator(`${workspaceNewSessionSelector(next)}, ${workspaceNewSessionSelector(raw)}`).first()
|
|
76
|
+
await expect(btn).toBeVisible()
|
|
77
|
+
await btn.click({ force: true })
|
|
78
|
+
|
|
79
|
+
await waitSession(page, { directory: space })
|
|
80
|
+
|
|
81
|
+
// Create a session by sending a prompt
|
|
82
|
+
const prompt = page.locator(promptSelector)
|
|
83
|
+
await expect(prompt).toBeVisible()
|
|
84
|
+
await prompt.fill("test")
|
|
85
|
+
await page.keyboard.press("Enter")
|
|
86
|
+
|
|
87
|
+
// Wait for the URL to update with the new session ID
|
|
88
|
+
await expect.poll(() => sessionIDFromUrl(page.url()) ?? "", { timeout: 15_000 }).not.toBe("")
|
|
89
|
+
|
|
90
|
+
const created = sessionIDFromUrl(page.url())
|
|
91
|
+
if (!created) throw new Error(`Failed to get session ID from url: ${page.url()}`)
|
|
92
|
+
trackSession(created, space)
|
|
93
|
+
await waitSessionSaved(space, created)
|
|
94
|
+
|
|
95
|
+
await expect(page).toHaveURL(new RegExp(`/${next}/session/${created}(?:[/?#]|$)`))
|
|
96
|
+
|
|
97
|
+
await openSidebar(page)
|
|
98
|
+
|
|
99
|
+
const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
|
|
100
|
+
await expect(otherButton).toBeVisible()
|
|
101
|
+
await otherButton.click({ force: true })
|
|
102
|
+
await waitSession(page, { directory: other })
|
|
103
|
+
|
|
104
|
+
const rootButton = page.locator(projectSwitchSelector(slug)).first()
|
|
105
|
+
await expect(rootButton).toBeVisible()
|
|
106
|
+
await rootButton.click({ force: true })
|
|
107
|
+
|
|
108
|
+
await waitSession(page, { directory: space, sessionID: created })
|
|
109
|
+
await expect(page).toHaveURL(new RegExp(`/session/${created}(?:[/?#]|$)`))
|
|
110
|
+
},
|
|
111
|
+
{ extra: [other] },
|
|
112
|
+
)
|
|
113
|
+
} finally {
|
|
114
|
+
await cleanupTestProject(other)
|
|
115
|
+
}
|
|
116
|
+
})
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { Page } from "@playwright/test"
|
|
2
|
+
import { test, expect } from "../fixtures"
|
|
3
|
+
import {
|
|
4
|
+
openSidebar,
|
|
5
|
+
resolveSlug,
|
|
6
|
+
sessionIDFromUrl,
|
|
7
|
+
setWorkspacesEnabled,
|
|
8
|
+
waitDir,
|
|
9
|
+
waitSession,
|
|
10
|
+
waitSessionSaved,
|
|
11
|
+
waitSlug,
|
|
12
|
+
} from "../actions"
|
|
13
|
+
import { promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
|
|
14
|
+
import { createSdk } from "../utils"
|
|
15
|
+
|
|
16
|
+
function item(space: { slug: string; raw: string }) {
|
|
17
|
+
return `${workspaceItemSelector(space.slug)}, ${workspaceItemSelector(space.raw)}`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function button(space: { slug: string; raw: string }) {
|
|
21
|
+
return `${workspaceNewSessionSelector(space.slug)}, ${workspaceNewSessionSelector(space.raw)}`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function waitWorkspaceReady(page: Page, space: { slug: string; raw: string }) {
|
|
25
|
+
await openSidebar(page)
|
|
26
|
+
await expect(page.locator(item(space)).first()).toBeVisible({ timeout: 60_000 })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function createWorkspace(page: Page, root: string, seen: string[]) {
|
|
30
|
+
await openSidebar(page)
|
|
31
|
+
await page.getByRole("button", { name: "New workspace" }).first().click()
|
|
32
|
+
|
|
33
|
+
const next = await resolveSlug(await waitSlug(page, [root, ...seen]))
|
|
34
|
+
await waitDir(page, next.directory)
|
|
35
|
+
return next
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function openWorkspaceNewSession(page: Page, space: { slug: string; raw: string; directory: string }) {
|
|
39
|
+
await waitWorkspaceReady(page, space)
|
|
40
|
+
|
|
41
|
+
const row = page.locator(item(space)).first()
|
|
42
|
+
await row.hover()
|
|
43
|
+
|
|
44
|
+
const next = page.locator(button(space)).first()
|
|
45
|
+
await expect(next).toBeVisible()
|
|
46
|
+
await next.click({ force: true })
|
|
47
|
+
|
|
48
|
+
await waitSession(page, { directory: space.directory })
|
|
49
|
+
await expect.poll(() => sessionIDFromUrl(page.url()) ?? "").toBe("")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function createSessionFromWorkspace(
|
|
53
|
+
page: Page,
|
|
54
|
+
space: { slug: string; raw: string; directory: string },
|
|
55
|
+
text: string,
|
|
56
|
+
) {
|
|
57
|
+
await openWorkspaceNewSession(page, space)
|
|
58
|
+
|
|
59
|
+
const prompt = page.locator(promptSelector)
|
|
60
|
+
await expect(prompt).toBeVisible()
|
|
61
|
+
await prompt.fill(text)
|
|
62
|
+
await page.keyboard.press("Enter")
|
|
63
|
+
|
|
64
|
+
await expect.poll(() => sessionIDFromUrl(page.url()) ?? "", { timeout: 15_000 }).not.toBe("")
|
|
65
|
+
const sessionID = sessionIDFromUrl(page.url())
|
|
66
|
+
if (!sessionID) throw new Error(`Failed to parse session id from url: ${page.url()}`)
|
|
67
|
+
|
|
68
|
+
await waitSessionSaved(space.directory, sessionID)
|
|
69
|
+
await createSdk(space.directory)
|
|
70
|
+
.session.abort({ sessionID })
|
|
71
|
+
.catch(() => undefined)
|
|
72
|
+
return sessionID
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
test("new sessions from sidebar workspace actions stay in selected workspace", async ({ page, withProject }) => {
|
|
76
|
+
await page.setViewportSize({ width: 1400, height: 800 })
|
|
77
|
+
|
|
78
|
+
await withProject(async ({ slug: root, trackDirectory, trackSession }) => {
|
|
79
|
+
await openSidebar(page)
|
|
80
|
+
await setWorkspacesEnabled(page, root, true)
|
|
81
|
+
|
|
82
|
+
const first = await createWorkspace(page, root, [])
|
|
83
|
+
trackDirectory(first.directory)
|
|
84
|
+
await waitWorkspaceReady(page, first)
|
|
85
|
+
|
|
86
|
+
const second = await createWorkspace(page, root, [first.slug])
|
|
87
|
+
trackDirectory(second.directory)
|
|
88
|
+
await waitWorkspaceReady(page, second)
|
|
89
|
+
|
|
90
|
+
trackSession(await createSessionFromWorkspace(page, first, `workspace one ${Date.now()}`), first.directory)
|
|
91
|
+
trackSession(await createSessionFromWorkspace(page, second, `workspace two ${Date.now()}`), second.directory)
|
|
92
|
+
trackSession(await createSessionFromWorkspace(page, first, `workspace one again ${Date.now()}`), first.directory)
|
|
93
|
+
})
|
|
94
|
+
})
|