Skip to content

Commit 48ea460

Browse files
authored
Merge branch 'anomalyco:dev' into dev
2 parents 514ec1b + 2db618d commit 48ea460

100 files changed

Lines changed: 5358 additions & 4406 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bun.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "AI-powered development tool",
55
"private": true,
66
"type": "module",
7-
"packageManager": "[email protected].9",
7+
"packageManager": "[email protected].5",
88
"scripts": {
99
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
1010
"dev:desktop": "bun --cwd packages/desktop tauri dev",
@@ -23,7 +23,7 @@
2323
"packages/slack"
2424
],
2525
"catalog": {
26-
"@types/bun": "1.3.9",
26+
"@types/bun": "1.3.5",
2727
"@octokit/rest": "22.0.0",
2828
"@hono/zod-validator": "0.4.2",
2929
"ulid": "3.0.1",

packages/app/e2e/files/file-open.spec.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,28 @@
11
import { test, expect } from "../fixtures"
2-
import { openPalette, clickListItem } from "../actions"
2+
import { promptSelector } from "../selectors"
33

44
test("can open a file tab from the search palette", async ({ page, gotoSession }) => {
55
await gotoSession()
66

7-
const dialog = await openPalette(page)
7+
await page.locator(promptSelector).click()
8+
await page.keyboard.type("/open")
9+
10+
const command = page.locator('[data-slash-id="file.open"]').first()
11+
await expect(command).toBeVisible()
12+
await page.keyboard.press("Enter")
13+
14+
const dialog = page
15+
.getByRole("dialog")
16+
.filter({ has: page.getByPlaceholder(/search files/i) })
17+
.first()
18+
await expect(dialog).toBeVisible()
819

920
const input = dialog.getByRole("textbox").first()
1021
await input.fill("package.json")
1122

12-
await clickListItem(dialog, { keyStartsWith: "file:" })
23+
const item = dialog.locator('[data-slot="list-item"][data-key^="file:"]').first()
24+
await expect(item).toBeVisible({ timeout: 30_000 })
25+
await item.click()
1326

1427
await expect(dialog).toHaveCount(0)
1528

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,41 @@
11
import { test, expect } from "../fixtures"
2-
import { openPalette, clickListItem } from "../actions"
2+
import { promptSelector } from "../selectors"
33

44
test("smoke file viewer renders real file content", async ({ page, gotoSession }) => {
55
await gotoSession()
66

7-
const sep = process.platform === "win32" ? "\\" : "/"
8-
const file = ["packages", "app", "package.json"].join(sep)
7+
await page.locator(promptSelector).click()
8+
await page.keyboard.type("/open")
99

10-
const dialog = await openPalette(page)
10+
const command = page.locator('[data-slash-id="file.open"]').first()
11+
await expect(command).toBeVisible()
12+
await page.keyboard.press("Enter")
1113

12-
const input = dialog.getByRole("textbox").first()
13-
await input.fill(file)
14+
const dialog = page
15+
.getByRole("dialog")
16+
.filter({ has: page.getByPlaceholder(/search files/i) })
17+
.first()
18+
await expect(dialog).toBeVisible()
1419

15-
await clickListItem(dialog, { text: /packages.*app.*package.json/ })
20+
const input = dialog.getByRole("textbox").first()
21+
await input.fill("package.json")
22+
23+
const items = dialog.locator('[data-slot="list-item"][data-key^="file:"]')
24+
let index = -1
25+
await expect
26+
.poll(
27+
async () => {
28+
const keys = await items.evaluateAll((nodes) => nodes.map((node) => node.getAttribute("data-key") ?? ""))
29+
index = keys.findIndex((key) => /packages[\\/]+app[\\/]+package\.json$/i.test(key.replace(/^file:/, "")))
30+
return index >= 0
31+
},
32+
{ timeout: 30_000 },
33+
)
34+
.toBe(true)
35+
36+
const item = items.nth(index)
37+
await expect(item).toBeVisible()
38+
await item.click()
1639

1740
await expect(dialog).toHaveCount(0)
1841

@@ -22,5 +45,5 @@ test("smoke file viewer renders real file content", async ({ page, gotoSession }
2245

2346
const code = page.locator('[data-component="code"]').first()
2447
await expect(code).toBeVisible()
25-
await expect(code.getByText("@opencode-ai/app")).toBeVisible()
48+
await expect(code.getByText(/"name"\s*:\s*"@opencode-ai\/app"/)).toBeVisible()
2649
})

packages/app/e2e/projects/workspace-new-session.spec.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,19 @@ async function createSessionFromWorkspace(page: Page, slug: string, text: string
6969

7070
const prompt = page.locator(promptSelector)
7171
await expect(prompt).toBeVisible()
72+
await expect(prompt).toBeEditable()
7273
await prompt.click()
73-
await page.keyboard.type(text)
74-
await page.keyboard.press("Enter")
74+
await expect(prompt).toBeFocused()
75+
await prompt.fill(text)
76+
await expect.poll(async () => ((await prompt.textContent()) ?? "").trim()).toContain(text)
77+
await prompt.press("Enter")
7578

7679
await expect.poll(() => slugFromUrl(page.url())).toBe(slug)
77-
await expect(page).toHaveURL(new RegExp(`/${slug}/session/[^/?#]+`), { timeout: 30_000 })
80+
await expect.poll(() => sessionIDFromUrl(page.url()) ?? "", { timeout: 30_000 }).not.toBe("")
7881

7982
const sessionID = sessionIDFromUrl(page.url())
8083
if (!sessionID) throw new Error(`Failed to parse session id from url: ${page.url()}`)
84+
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${sessionID}(?:[/?#]|$)`))
8185
return sessionID
8286
}
8387

packages/app/e2e/projects/workspaces.spec.ts

Lines changed: 60 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,12 @@ import {
1111
cleanupTestProject,
1212
clickMenuItem,
1313
confirmDialog,
14-
openProjectMenu,
1514
openSidebar,
1615
openWorkspaceMenu,
1716
setWorkspacesEnabled,
1817
} from "../actions"
19-
import {
20-
inlineInputSelector,
21-
projectSwitchSelector,
22-
projectWorkspacesToggleSelector,
23-
workspaceItemSelector,
24-
} from "../selectors"
25-
import { dirSlug } from "../utils"
18+
import { dropdownMenuContentSelector, inlineInputSelector, workspaceItemSelector } from "../selectors"
19+
import { createSdk, dirSlug } from "../utils"
2620

2721
function slugFromUrl(url: string) {
2822
return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
@@ -143,26 +137,35 @@ test("non-git projects keep workspace mode disabled", async ({ page, withProject
143137
await fs.writeFile(path.join(nonGit, "README.md"), "# e2e nongit\n")
144138

145139
try {
146-
await withProject(
147-
async () => {
148-
await openSidebar(page)
140+
await withProject(async () => {
141+
await page.goto(`/${nonGitSlug}/session`)
142+
143+
await expect.poll(() => slugFromUrl(page.url()), { timeout: 30_000 }).not.toBe("")
149144

150-
const nonGitButton = page.locator(projectSwitchSelector(nonGitSlug)).first()
151-
await expect(nonGitButton).toBeVisible()
152-
await nonGitButton.click()
153-
await expect(page).toHaveURL(new RegExp(`/${nonGitSlug}/session`))
145+
const activeDir = base64Decode(slugFromUrl(page.url()))
146+
expect(path.basename(activeDir)).toContain("opencode-e2e-project-nongit-")
154147

155-
const menu = await openProjectMenu(page, nonGitSlug)
156-
const toggle = menu.locator(projectWorkspacesToggleSelector(nonGitSlug)).first()
148+
await openSidebar(page)
149+
await expect(page.getByRole("button", { name: "New workspace" })).toHaveCount(0)
157150

158-
await expect(toggle).toBeVisible()
159-
await expect(toggle).toBeDisabled()
151+
const trigger = page.locator('[data-action="project-menu"]').first()
152+
const hasMenu = await trigger
153+
.isVisible()
154+
.then((x) => x)
155+
.catch(() => false)
156+
if (!hasMenu) return
160157

161-
await expect(menu.getByRole("menuitem", { name: "New workspace" })).toHaveCount(0)
162-
await expect(page.getByRole("button", { name: "New workspace" })).toHaveCount(0)
163-
},
164-
{ extra: [nonGit] },
165-
)
158+
await trigger.click({ force: true })
159+
160+
const menu = page.locator(dropdownMenuContentSelector).first()
161+
await expect(menu).toBeVisible()
162+
163+
const toggle = menu.locator('[data-action="project-workspaces-toggle"]').first()
164+
165+
await expect(toggle).toBeVisible()
166+
await expect(toggle).toBeDisabled()
167+
await expect(menu.getByRole("menuitem", { name: "New workspace" })).toHaveCount(0)
168+
})
166169
} finally {
167170
await cleanupTestProject(nonGit)
168171
}
@@ -256,14 +259,45 @@ test("can delete a workspace", async ({ page, withProject }) => {
256259
await page.setViewportSize({ width: 1400, height: 800 })
257260

258261
await withProject(async (project) => {
259-
const { rootSlug, slug } = await setupWorkspaceTest(page, project)
262+
const sdk = createSdk(project.directory)
263+
const { rootSlug, slug, directory } = await setupWorkspaceTest(page, project)
264+
265+
await expect
266+
.poll(
267+
async () => {
268+
const worktrees = await sdk.worktree
269+
.list()
270+
.then((r) => r.data ?? [])
271+
.catch(() => [] as string[])
272+
return worktrees.includes(directory)
273+
},
274+
{ timeout: 30_000 },
275+
)
276+
.toBe(true)
260277

261278
const menu = await openWorkspaceMenu(page, slug)
262279
await clickMenuItem(menu, /^Delete$/i, { force: true })
263280
await confirmDialog(page, /^Delete workspace$/i)
264281

265282
await expect(page).toHaveURL(new RegExp(`/${rootSlug}/session`))
266-
await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0)
283+
284+
await expect
285+
.poll(
286+
async () => {
287+
const worktrees = await sdk.worktree
288+
.list()
289+
.then((r) => r.data ?? [])
290+
.catch(() => [] as string[])
291+
return worktrees.includes(directory)
292+
},
293+
{ timeout: 60_000 },
294+
)
295+
.toBe(false)
296+
297+
await project.gotoSession()
298+
299+
await openSidebar(page)
300+
await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0, { timeout: 60_000 })
267301
await expect(page.locator(workspaceItemSelector(rootSlug)).first()).toBeVisible()
268302
})
269303
})
Lines changed: 78 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,95 @@
11
import { test, expect } from "../fixtures"
2+
import type { Page } from "@playwright/test"
23
import { promptSelector } from "../selectors"
34
import { withSession } from "../actions"
45

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+
535
test("context panel can be opened from the prompt", async ({ page, sdk, gotoSession }) => {
636
const title = `e2e smoke context ${Date.now()}`
737

838
await withSession(sdk, title, async (session) => {
9-
await sdk.session.promptAsync({
10-
sessionID: session.id,
11-
noReply: true,
12-
parts: [
13-
{
14-
type: "text",
15-
text: "seed context",
16-
},
17-
],
18-
})
39+
await seedContextSession({ sessionID: session.id, sdk })
1940

20-
await expect
21-
.poll(async () => {
22-
const messages = await sdk.session.messages({ sessionID: session.id, limit: 1 }).then((r) => r.data ?? [])
23-
return messages.length
24-
})
25-
.toBeGreaterThan(0)
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+
})
2651

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 })
2755
await gotoSession(session.id)
2856

29-
const contextButton = page
30-
.locator('[data-component="button"]')
31-
.filter({ has: page.locator('[data-component="progress-circle"]').first() })
32-
.first()
57+
await page.locator(promptSelector).click()
3358

34-
await expect(contextButton).toBeVisible()
35-
await contextButton.click()
59+
const trigger = contextButton(page)
60+
await expect(trigger).toBeVisible()
61+
await trigger.click()
3662

3763
const tabs = page.locator('[data-component="tabs"][data-variant="normal"]')
38-
await expect(tabs.getByRole("tab", { name: "Context" })).toBeVisible()
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)
3994
})
4095
})

0 commit comments

Comments
 (0)